From 61ecd4b4f80905c6c5e1f0f0bfc925472e50a50f Mon Sep 17 00:00:00 2001 From: Craigory Coppola Date: Mon, 26 Aug 2024 18:28:16 -0400 Subject: [PATCH] fix(core): negative workspace-root filesets should override positive filesets (#27524) --- packages/nx/src/command-line/graph/graph.ts | 64 ++++++--- .../__snapshots__/task-hasher.spec.ts.snap | 132 ++++++------------ .../hasher/native-task-hasher-impl.spec.ts | 58 +++----- .../nx/src/hasher/node-task-hasher-impl.ts | 58 +++++--- packages/nx/src/native/tasks/hash_planner.rs | 23 +-- .../tasks/hashers/hash_workspace_files.rs | 56 +++++--- packages/nx/src/native/tasks/types.rs | 5 +- .../tests/__snapshots__/planner.spec.ts.snap | 46 ++---- .../target-project-locator.spec.ts | 9 +- 9 files changed, 224 insertions(+), 227 deletions(-) diff --git a/packages/nx/src/command-line/graph/graph.ts b/packages/nx/src/command-line/graph/graph.ts index ca8c4edda2284..583e62c89a370 100644 --- a/packages/nx/src/command-line/graph/graph.ts +++ b/packages/nx/src/command-line/graph/graph.ts @@ -1064,8 +1064,10 @@ function expandInputs( const externalInputs: string[] = []; const otherInputs: string[] = []; inputs.forEach((input) => { - if (input.startsWith('{workspaceRoot}')) { - workspaceRootInputs.push(input); + // grouped workspace inputs look like workspace:[pattern,otherPattern] + if (input.startsWith('workspace:[')) { + const inputs = input.substring(11, input.length - 1).split(','); + workspaceRootInputs.push(...inputs); return; } const maybeProjectName = input.split(':')[0]; @@ -1088,24 +1090,9 @@ function expandInputs( } }); - const workspaceRootsExpanded: string[] = workspaceRootInputs.flatMap( - (input) => { - const matches = []; - const withoutWorkspaceRoot = input.substring(16); - const matchingFile = allWorkspaceFiles.find( - (t) => t.file === withoutWorkspaceRoot - ); - if (matchingFile) { - matches.push(matchingFile.file); - } else { - allWorkspaceFiles - .filter((f) => minimatch(f.file, withoutWorkspaceRoot)) - .forEach((f) => { - matches.push(f.file); - }); - } - return matches; - } + const workspaceRootsExpanded: string[] = getExpandedWorkspaceRoots( + workspaceRootInputs, + allWorkspaceFiles ); const otherInputsExpanded = otherInputs.map((input) => { @@ -1173,6 +1160,43 @@ export interface GraphJson { graph: ProjectGraph; } +function getExpandedWorkspaceRoots( + workspaceRootInputs: string[], + allWorkspaceFiles: FileData[] +) { + const workspaceRootsExpanded: string[] = []; + const negativeWRPatterns = []; + const positiveWRPatterns = []; + for (const fileset of workspaceRootInputs) { + if (fileset.startsWith('!')) { + negativeWRPatterns.push(fileset.substring(17)); + } else { + positiveWRPatterns.push(fileset.substring(16)); + } + } + for (const pattern of positiveWRPatterns) { + const matchingFile = allWorkspaceFiles.find((t) => t.file === pattern); + if ( + matchingFile && + !negativeWRPatterns.some((p) => minimatch(matchingFile.file, p)) + ) { + workspaceRootsExpanded.push(matchingFile.file); + } else { + allWorkspaceFiles + .filter( + (f) => + minimatch(f.file, pattern) && + !negativeWRPatterns.some((p) => minimatch(f.file, p)) + ) + .forEach((f) => { + workspaceRootsExpanded.push(f.file); + }); + } + } + workspaceRootsExpanded.sort(); + return workspaceRootsExpanded; +} + async function createJsonOutput( prunedGraph: ProjectGraph, rawGraph: ProjectGraph, diff --git a/packages/nx/src/hasher/__snapshots__/task-hasher.spec.ts.snap b/packages/nx/src/hasher/__snapshots__/task-hasher.spec.ts.snap index 12179acf50b1b..87f36aaa4562e 100644 --- a/packages/nx/src/hasher/__snapshots__/task-hasher.spec.ts.snap +++ b/packages/nx/src/hasher/__snapshots__/task-hasher.spec.ts.snap @@ -12,13 +12,11 @@ exports[`TaskHasher dependentTasksOutputFiles should depend on dependent tasks o "parent:!{projectRoot}/**/*.spec.ts": "17962802443644575456", "parent:ProjectConfiguration": "16743571606168248002", "parent:TsConfig": "8767608672024750088", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "14131423056691033336", + "value": "13814139315500456230", } `; @@ -34,13 +32,11 @@ exports[`TaskHasher dependentTasksOutputFiles should work with dependent tasks w "parent:!{projectRoot}/**/*.spec.ts": "17962802443644575456", "parent:ProjectConfiguration": "16743571606168248002", "parent:TsConfig": "8767608672024750088", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "14131423056691033336", + "value": "13814139315500456230", } `; @@ -57,13 +53,11 @@ exports[`TaskHasher hashTarget should hash entire subtree in a deterministic way "npm:packageA": "$packageA0.0.0$", "npm:packageB": "$packageB0.0.0$", "npm:packageC": "$packageC0.0.0$", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "12756041818139421941", + "value": "13769080483891190943", } `; @@ -80,13 +74,11 @@ exports[`TaskHasher hashTarget should hash entire subtree in a deterministic way "npm:packageA": "$packageA0.0.0$", "npm:packageB": "$packageB0.0.0$", "npm:packageC": "$packageC0.0.0$", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "15326312070983573452", + "value": "16958448572045958975", } `; @@ -103,13 +95,11 @@ exports[`TaskHasher hashTarget should hash entire subtree of dependencies 1`] = "npm:@nx/webpack": "$nx/webpack16$", "npm:nx": "$nx16$", "npm:webpack": "5.0.0", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "14419327228911184578", + "value": "10473676482354003739", } `; @@ -123,13 +113,11 @@ exports[`TaskHasher hashTarget should hash executor dependencies of @nx packages "app:TsConfig": "8767608672024750088", "app:{projectRoot}/**/*": "3244421341483603138", "npm:@nx/webpack": "16.0.0", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "379625642227035180", + "value": "12591571137663595931", } `; @@ -145,13 +133,11 @@ exports[`TaskHasher hashTarget should use externalDependencies to override nx:ru "env:undefined": "3244421341483603138", "npm:react": "17.0.0", "npm:webpack": "5.0.0", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "14779270419297346086", + "value": "12439359954646788830", } `; @@ -165,13 +151,11 @@ exports[`TaskHasher hashTarget should use externalDependencies with empty array "app:TsConfig": "8767608672024750088", "app:{projectRoot}/**/*": "3244421341483603138", "env:undefined": "3244421341483603138", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "13520777692097937224", + "value": "12962045506359360703", } `; @@ -189,15 +173,13 @@ exports[`TaskHasher should be able to handle multiple filesets per project 1`] = "parent:ProjectConfiguration": "11407237178360979685", "parent:TsConfig": "8767608672024750088", "parent:{projectRoot}/**/*": "7263479247245830838", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/global1": "3052102066027208710", - "{workspaceRoot}/global2": "8197394511443659629", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/global1]": "3052102066027208710", + "workspace:[{workspaceRoot}/global2]": "8197394511443659629", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "18157475608082023962", + "value": "225950192185398171", } `; @@ -211,14 +193,12 @@ exports[`TaskHasher should be able to handle multiple filesets per project 2`] = "child:ProjectConfiguration": "17211930887387929067", "child:TsConfig": "8767608672024750088", "child:{projectRoot}/**/*": "2300207741412661544", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/global1": "3052102066027208710", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/global1]": "3052102066027208710", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "13395934251559762209", + "value": "6479833018413810049", } `; @@ -234,13 +214,11 @@ exports[`TaskHasher should be able to include only a part of the base tsconfig 1 "parent:{projectRoot}/**/*": "8263681721738113012", "runtime:echo runtime123": "29846575039086708", "runtime:echo runtime456": "9687767313975325934", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "11657880113460718404", + "value": "2591807031038621869", } `; @@ -264,13 +242,11 @@ exports[`TaskHasher should create task hash 1`] = ` "unrelated:ProjectConfiguration": "9857815511578358695", "unrelated:TsConfig": "8767608672024750088", "unrelated:{projectRoot}/**/*": "10091615118977982257", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "11744133219421970738", + "value": "13363019766081493373", } `; @@ -285,13 +261,11 @@ exports[`TaskHasher should hash missing dependent npm project versions 1`] = ` "app:TsConfig": "8767608672024750088", "app:{projectRoot}/**/*": "9104199730100321982", "npm:react": "17.0.0", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "69684997513271647", + "value": "1259505935462026504", } `; @@ -305,13 +279,11 @@ exports[`TaskHasher should hash multiple filesets of a project 1`] = ` "parent:ProjectConfiguration": "10499856664466672714", "parent:TsConfig": "8767608672024750088", "parent:{projectRoot}/**/*": "7263479247245830838", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "4923065065541230169", + "value": "18165704585933725119", } `; @@ -325,13 +297,11 @@ exports[`TaskHasher should hash multiple filesets of a project 2`] = ` "parent:!{projectRoot}/**/*.spec.ts": "17962802443644575456", "parent:ProjectConfiguration": "10499856664466672714", "parent:TsConfig": "8767608672024750088", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "4731520952840688799", + "value": "8235745364524428309", } `; @@ -348,13 +318,11 @@ exports[`TaskHasher should hash non-default filesets 1`] = ` "parent:!{projectRoot}/**/*.spec.ts": "17962802443644575456", "parent:ProjectConfiguration": "7903062777922836147", "parent:TsConfig": "8767608672024750088", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "2706545192249046366", + "value": "15360225209266319721", } `; @@ -369,13 +337,11 @@ exports[`TaskHasher should hash npm project versions 1`] = ` "app:TsConfig": "8767608672024750088", "app:{projectRoot}/**/*": "9104199730100321982", "npm:react": "17.0.0", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "69684997513271647", + "value": "1259505935462026504", } `; @@ -392,13 +358,11 @@ exports[`TaskHasher should hash task where the project has dependencies 1`] = ` "parent:ProjectConfiguration": "1818520398670323053", "parent:TsConfig": "8767608672024750088", "parent:{projectRoot}/**/*": "14822394489351823627", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "10890020828938010310", + "value": "9343642962263515428", } `; @@ -415,13 +379,11 @@ exports[`TaskHasher should hash tasks where the project graph has circular depen "parent:ProjectConfiguration": "18166168584521190546", "parent:TsConfig": "8767608672024750088", "parent:{projectRoot}/**/*": "9104199730100321982", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "9112654928859037831", + "value": "7263142938698693594", } `; @@ -438,13 +400,11 @@ exports[`TaskHasher should hash tasks where the project graph has circular depen "parent:ProjectConfiguration": "18166168584521190546", "parent:TsConfig": "8767608672024750088", "parent:{projectRoot}/**/*": "9104199730100321982", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "144123652661336112", + "value": "3664247485971662150", } `; @@ -461,12 +421,10 @@ exports[`TaskHasher should use targetDefaults from nx.json 1`] = ` "parent:!{projectRoot}/**/*.spec.ts": "17962802443644575456", "parent:ProjectConfiguration": "18166168584521190546", "parent:TsConfig": "8767608672024750088", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "8942239360311677987", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "8942239360311677987", }, "runtime": {}, }, - "value": "5596126261759417547", + "value": "4674721715858855274", } `; diff --git a/packages/nx/src/hasher/native-task-hasher-impl.spec.ts b/packages/nx/src/hasher/native-task-hasher-impl.spec.ts index 30db683af3de9..32e6f6ddc2ce5 100644 --- a/packages/nx/src/hasher/native-task-hasher-impl.spec.ts +++ b/packages/nx/src/hasher/native-task-hasher-impl.spec.ts @@ -165,11 +165,9 @@ describe('native task hasher', () => { "unrelated:ProjectConfiguration": "11133337791644294114", "unrelated:TsConfig": "2264969541778889434", "unrelated:{projectRoot}/**/*": "10505120368757496776", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "5219582320960288192", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "5219582320960288192", }, - "value": "17193008237392864712", + "value": "13049022000906481001", }, ] `); @@ -232,11 +230,9 @@ describe('native task hasher', () => { "parent:ProjectConfiguration": "8031122597231773116", "parent:TsConfig": "2264969541778889434", "parent:{projectRoot}/**/*": "15295586939211629225", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "5219582320960288192", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "5219582320960288192", }, - "value": "4141725338792606519", + "value": "17442516481637512275", } `); }); @@ -313,11 +309,9 @@ describe('native task hasher', () => { "parent:!{projectRoot}/**/*.spec.ts": "7663204892242899157", "parent:ProjectConfiguration": "3608670998275221195", "parent:TsConfig": "2264969541778889434", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "4641558175996703359", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "4641558175996703359", }, - "value": "12061654175538209437", + "value": "3497993078654537309", } `); }); @@ -381,11 +375,9 @@ describe('native task hasher', () => { "parent:!{projectRoot}/**/*.spec.ts": "7663204892242899157", "parent:ProjectConfiguration": "16402137858974842465", "parent:TsConfig": "2264969541778889434", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "4641558175996703359", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "4641558175996703359", }, - "value": "1683972350273460485", + "value": "10775755355957559912", }, { "details": { @@ -393,11 +385,9 @@ describe('native task hasher', () => { "parent:ProjectConfiguration": "16402137858974842465", "parent:TsConfig": "2264969541778889434", "parent:{projectRoot}/**/*": "15295586939211629225", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "4641558175996703359", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "4641558175996703359", }, - "value": "2469956415584213984", + "value": "13219368697419749776", }, ] `); @@ -484,13 +474,11 @@ describe('native task hasher', () => { "parent:ProjectConfiguration": "14398811678394411425", "parent:TsConfig": "2264969541778889434", "parent:{projectRoot}/**/*": "15295586939211629225", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/global1": "13078141817211771580", - "{workspaceRoot}/global2": "13625885481717016690", - "{workspaceRoot}/nx.json": "10897751101872977225", + "workspace:[{workspaceRoot}/global1]": "13078141817211771580", + "workspace:[{workspaceRoot}/global2]": "13625885481717016690", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "10897751101872977225", }, - "value": "12563443797830627612", + "value": "14298810822113951946", }, ] `); @@ -542,11 +530,9 @@ describe('native task hasher', () => { "parent:ProjectConfiguration": "3608670998275221195", "parent:TsConfig": "8661678577354855152", "parent:{projectRoot}/**/*": "15295586939211629225", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "5219582320960288192", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "5219582320960288192", }, - "value": "192468752006013407", + "value": "10821775409399212451", } `); }); @@ -622,11 +608,9 @@ describe('native task hasher', () => { "parent:ProjectConfiguration": "3608670998275221195", "parent:TsConfig": "2264969541778889434", "parent:{projectRoot}/**/*": "15295586939211629225", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "5219582320960288192", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "5219582320960288192", }, - "value": "571545311225175014", + "value": "12197760444984597111", } `); @@ -646,11 +630,9 @@ describe('native task hasher', () => { "parent:ProjectConfiguration": "3608670998275221195", "parent:TsConfig": "2264969541778889434", "parent:{projectRoot}/**/*": "15295586939211629225", - "{workspaceRoot}/.gitignore": "3244421341483603138", - "{workspaceRoot}/.nxignore": "3244421341483603138", - "{workspaceRoot}/nx.json": "5219582320960288192", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]": "5219582320960288192", }, - "value": "571545311225175014", + "value": "12197760444984597111", } `); }); diff --git a/packages/nx/src/hasher/node-task-hasher-impl.ts b/packages/nx/src/hasher/node-task-hasher-impl.ts index 9114a3ea7b1f2..cc7b242560fea 100644 --- a/packages/nx/src/hasher/node-task-hasher-impl.ts +++ b/packages/nx/src/hasher/node-task-hasher-impl.ts @@ -499,10 +499,10 @@ export class NodeTaskHasherImpl implements TaskHasherImpl { this.hashProjectFileset(projectName, projectFilesets), this.hashProjectConfig(projectName), this.hashTsConfig(projectName), - ...[ - ...workspaceFilesets, - ...this.legacyFilesetInputs.map((r) => r.fileset), - ].map((fileset) => this.hashRootFileset(fileset)), + ...(workspaceFilesets.length + ? [this.hashRootFilesets(workspaceFilesets)] + : []), + this.hashRootFilesets(this.legacyFilesetInputs.map((r) => r.fileset)), ...[...notFilesets, ...this.legacyRuntimeInputs].map((r) => r['runtime'] ? this.hashRuntime(env, r['runtime']) @@ -538,23 +538,45 @@ export class NodeTaskHasherImpl implements TaskHasherImpl { return Promise.all(partialHashes).then((hashes) => hashes.flat()); } - private async hashRootFileset(fileset: string): Promise { - const mapKey = fileset; - const withoutWorkspaceRoot = fileset.substring(16); + private async hashRootFilesets(filesets: string[]): Promise { + const mapKey = `workspace:[${filesets.join(',')}]`; if (!this.filesetHashes[mapKey]) { this.filesetHashes[mapKey] = new Promise(async (res) => { const parts = []; - const matchingFile = this.allWorkspaceFiles.find( - (t) => t.file === withoutWorkspaceRoot - ); - if (matchingFile) { - parts.push(matchingFile.hash); - } else { - this.allWorkspaceFiles - .filter((f) => minimatch(f.file, withoutWorkspaceRoot)) - .forEach((f) => { - parts.push(f.hash); - }); + const negativePatterns = []; + const positivePatterns = []; + for (const fileset of filesets) { + if (fileset.startsWith('!')) { + negativePatterns.push(fileset.substring(17)); + } else { + positivePatterns.push(fileset.substring(16)); + } + } + for (const fileset of positivePatterns) { + const withoutWorkspaceRoot = fileset; + // Used to shortcut minimatch if not necessary + const matchingFile = this.allWorkspaceFiles.find( + (t) => t.file === withoutWorkspaceRoot + ); + // shortcut because there is a direct match + if (matchingFile) { + if ( + !negativePatterns.some((p) => minimatch(matchingFile.file, p)) + ) { + parts.push(matchingFile.hash); + } + // No direct match, check if pattern matched + } else { + this.allWorkspaceFiles + .filter( + (f) => + minimatch(f.file, withoutWorkspaceRoot) && + !negativePatterns.some((p) => minimatch(f.file, p)) + ) + .forEach((f) => { + parts.push(f.hash); + }); + } } const value = hashArray(parts); res({ diff --git a/packages/nx/src/native/tasks/hash_planner.rs b/packages/nx/src/native/tasks/hash_planner.rs index 8a458a4b2c0fb..6a38be96aa9ee 100644 --- a/packages/nx/src/native/tasks/hash_planner.rs +++ b/packages/nx/src/native/tasks/hash_planner.rs @@ -71,11 +71,11 @@ impl HashPlanner { let mut inputs: Vec = target .unwrap_or(vec![]) .into_iter() - .chain(vec![ - HashInstruction::WorkspaceFileSet("{workspaceRoot}/nx.json".to_string()), - HashInstruction::WorkspaceFileSet("{workspaceRoot}/.gitignore".to_string()), - HashInstruction::WorkspaceFileSet("{workspaceRoot}/.nxignore".to_string()), - ]) + .chain(vec![HashInstruction::WorkspaceFileSet(vec![ + "{workspaceRoot}/nx.json".to_string(), + "{workspaceRoot}/.gitignore".to_string(), + "{workspaceRoot}/.nxignore".to_string(), + ])]) .chain(self_inputs) .collect(); @@ -326,7 +326,11 @@ impl HashPlanner { }); let project_file_set_inputs = project_file_set_inputs(project_name, project_file_sets); - let workspace_file_set_inputs = workspace_file_set_inputs(workspace_file_sets); + // let workspace_file_set_inputs = workspace_file_set_inputs(workspace_file_sets); + let workspace_file_set_inputs = match workspace_file_sets.is_empty() { + true => vec![], + false => vec![workspace_file_set_inputs(workspace_file_sets)], + }; let runtime_and_env_inputs = self_inputs.iter().filter_map(|i| match i { Input::Runtime(runtime) => Some(HashInstruction::Runtime(runtime.to_string())), Input::Environment(env) => Some(HashInstruction::Environment(env.to_string())), @@ -430,9 +434,6 @@ fn project_file_set_inputs(project_name: &str, file_sets: Vec<&str>) -> Vec) -> Vec { - file_sets - .into_iter() - .map(|s| HashInstruction::WorkspaceFileSet(s.to_string())) - .collect() +fn workspace_file_set_inputs(file_sets: Vec<&str>) -> HashInstruction { + HashInstruction::WorkspaceFileSet(file_sets.iter().map(|f| f.to_string()).collect()) } diff --git a/packages/nx/src/native/tasks/hashers/hash_workspace_files.rs b/packages/nx/src/native/tasks/hashers/hash_workspace_files.rs index f32ef0b727ac0..2535718fb8cca 100644 --- a/packages/nx/src/native/tasks/hashers/hash_workspace_files.rs +++ b/packages/nx/src/native/tasks/hashers/hash_workspace_files.rs @@ -8,37 +8,57 @@ use crate::native::types::FileData; use crate::native::{glob::build_glob_set, hasher::hash}; pub fn hash_workspace_files( - workspace_file_set: &str, + workspace_file_sets: &[String], all_workspace_files: &[FileData], cache: Arc>, ) -> Result { - let file_set = workspace_file_set.strip_prefix("{workspaceRoot}/"); + let globs: Vec = workspace_file_sets + .iter() + .inspect(|&x| trace!("Workspace file set: {}", x)) + .filter_map(|x| { + let is_negative = x.starts_with("!"); + let x = if is_negative { &x[1..] } else { x }; + let fileset: Option<&str> = x.strip_prefix("{workspaceRoot}/"); + if let Some(fileset) = fileset { + if is_negative { + Some(format!("!{}", fileset)) + } else { + Some(fileset.to_string()) + } + } else { + warn!( + "{x} does not start with {}. This will throw an error in Nx 20.", + "{workspaceRoot}/" + ); + None + } + }) + .collect(); - let Some(file_set) = file_set else { - warn!( - "{workspace_file_set} does not start with {}. This will throw an error in Nx 20.", - "{workspaceRoot}/" - ); + if globs.is_empty() { return Ok(hash(b"")); - }; + } - if let Some(cache_results) = cache.get(file_set) { + let cache_key = globs.join(","); + if let Some(cache_results) = cache.get(&cache_key) { return Ok(cache_results.clone()); } - let glob = build_glob_set(&[file_set])?; + let glob = build_glob_set(&globs)?; let mut hasher = xxhash_rust::xxh3::Xxh3::new(); + let mut hashes: Vec = Vec::new(); for file in all_workspace_files .iter() .filter(|file| glob.is_match(&file.file)) { - trace!("{:?} was found with glob {:?}", file.file, file_set); - hasher.update(file.hash.as_bytes()); + trace!("{:?} was found with glob {:?}", file.file, globs); + hashes.push(file.hash.clone()) } + hasher.update(hashes.join(",").as_bytes()); let hashed_value = hasher.digest().to_string(); - cache.insert(file_set.to_string(), hashed_value.clone()); + cache.insert(cache_key.to_string(), hashed_value.clone()); Ok(hashed_value) } @@ -52,8 +72,12 @@ mod test { #[test] fn invalid_workspace_input_is_just_empty_hash() { - let result = - hash_workspace_files("packages/{package}", &[], Arc::new(DashMap::new())).unwrap(); + let result = hash_workspace_files( + &["packages/{package}".to_string()], + &[], + Arc::new(DashMap::new()), + ) + .unwrap(); assert_eq!(result, hash(b"")); } @@ -76,7 +100,7 @@ mod test { hash: "abc".into(), }; let result = hash_workspace_files( - "{workspaceRoot}/.gitignore", + &["{workspaceRoot}/.gitignore".to_string()], &[ gitignore_file.clone(), nxignore_file.clone(), diff --git a/packages/nx/src/native/tasks/types.rs b/packages/nx/src/native/tasks/types.rs index a3fff3e0596d8..9c90c2e1e201b 100644 --- a/packages/nx/src/native/tasks/types.rs +++ b/packages/nx/src/native/tasks/types.rs @@ -32,7 +32,7 @@ pub struct TaskGraph { #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum HashInstruction { - WorkspaceFileSet(String), + WorkspaceFileSet(Vec), Runtime(String), Environment(String), ProjectFileSet(String, Vec), @@ -73,7 +73,8 @@ impl fmt::Display for HashInstruction { HashInstruction::ProjectFileSet(project_name, file_set) => { format!("{project_name}:{}", file_set.join(",")) } - HashInstruction::WorkspaceFileSet(file_set) => file_set.to_string(), + HashInstruction::WorkspaceFileSet(file_set) => + format!("workspace:[{}]", file_set.join(",")), HashInstruction::Runtime(runtime) => format!("runtime:{}", runtime), HashInstruction::Environment(env) => format!("env:{}", env), HashInstruction::TaskOutput(task_output, dep_outputs) => { diff --git a/packages/nx/src/native/tests/__snapshots__/planner.spec.ts.snap b/packages/nx/src/native/tests/__snapshots__/planner.spec.ts.snap index 4f6e011bb330b..6b85614467105 100644 --- a/packages/nx/src/native/tests/__snapshots__/planner.spec.ts.snap +++ b/packages/nx/src/native/tests/__snapshots__/planner.spec.ts.snap @@ -9,9 +9,7 @@ exports[`task planner dependentTasksOutputFiles should depend on dependent tasks "parent:!{projectRoot}/**/*.spec.ts", "parent:ProjectConfiguration", "parent:TsConfig", - "{workspaceRoot}/.gitignore", - "{workspaceRoot}/.nxignore", - "{workspaceRoot}/nx.json", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]", ], } `; @@ -27,11 +25,9 @@ exports[`task planner should be able to handle multiple filesets per project 1`] "parent:ProjectConfiguration", "parent:TsConfig", "parent:{projectRoot}/**/*", - "{workspaceRoot}/.gitignore", - "{workspaceRoot}/.nxignore", - "{workspaceRoot}/global1", - "{workspaceRoot}/global2", - "{workspaceRoot}/nx.json", + "workspace:[{workspaceRoot}/global1]", + "workspace:[{workspaceRoot}/global2]", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]", ], } `; @@ -46,9 +42,7 @@ exports[`task planner should build plans where the project graph has circular de "parent:ProjectConfiguration", "parent:TsConfig", "parent:{projectRoot}/**/*", - "{workspaceRoot}/.gitignore", - "{workspaceRoot}/.nxignore", - "{workspaceRoot}/nx.json", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]", ], "parent:build": [ "AllExternalDependencies", @@ -58,9 +52,7 @@ exports[`task planner should build plans where the project graph has circular de "parent:ProjectConfiguration", "parent:TsConfig", "parent:{projectRoot}/**/*", - "{workspaceRoot}/.gitignore", - "{workspaceRoot}/.nxignore", - "{workspaceRoot}/nx.json", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]", ], } `; @@ -73,10 +65,8 @@ exports[`task planner should hash executors 1`] = ` "proj:ProjectConfiguration", "proj:TsConfig", "proj:{projectRoot}/**/*", - "{workspaceRoot}/.gitignore", - "{workspaceRoot}/.nxignore", - "{workspaceRoot}/global1", - "{workspaceRoot}/nx.json", + "workspace:[{workspaceRoot}/global1]", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]", ], } `; @@ -89,9 +79,7 @@ exports[`task planner should include npm projects 1`] = ` "app:TsConfig", "app:{projectRoot}/**/*", "npm:react", - "{workspaceRoot}/.gitignore", - "{workspaceRoot}/.nxignore", - "{workspaceRoot}/nx.json", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]", ], } `; @@ -103,18 +91,14 @@ exports[`task planner should make a plan with multiple filesets of a project 1`] "parent:!{projectRoot}/**/*.spec.ts", "parent:ProjectConfiguration", "parent:TsConfig", - "{workspaceRoot}/.gitignore", - "{workspaceRoot}/.nxignore", - "{workspaceRoot}/nx.json", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]", ], "parent:test": [ "AllExternalDependencies", "parent:ProjectConfiguration", "parent:TsConfig", "parent:{projectRoot}/**/*", - "{workspaceRoot}/.gitignore", - "{workspaceRoot}/.nxignore", - "{workspaceRoot}/nx.json", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]", ], } `; @@ -129,9 +113,7 @@ exports[`task planner should plan non-default filesets 1`] = ` "parent:!{projectRoot}/**/*.spec.ts", "parent:ProjectConfiguration", "parent:TsConfig", - "{workspaceRoot}/.gitignore", - "{workspaceRoot}/.nxignore", - "{workspaceRoot}/nx.json", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]", ], } `; @@ -149,9 +131,7 @@ exports[`task planner should plan the task where the project has dependencies 1` "parent:ProjectConfiguration", "parent:TsConfig", "parent:{projectRoot}/**/*", - "{workspaceRoot}/.gitignore", - "{workspaceRoot}/.nxignore", - "{workspaceRoot}/nx.json", + "workspace:[{workspaceRoot}/nx.json,{workspaceRoot}/.gitignore,{workspaceRoot}/.nxignore]", ], } `; diff --git a/packages/nx/src/plugins/js/project-graph/build-dependencies/target-project-locator.spec.ts b/packages/nx/src/plugins/js/project-graph/build-dependencies/target-project-locator.spec.ts index 99b4a01cb8972..517a80f4a81c5 100644 --- a/packages/nx/src/plugins/js/project-graph/build-dependencies/target-project-locator.spec.ts +++ b/packages/nx/src/plugins/js/project-graph/build-dependencies/target-project-locator.spec.ts @@ -11,6 +11,8 @@ import { isBuiltinModuleImport, } from './target-project-locator'; +import { builtinModules } from 'node:module'; + jest.mock('@nx/devkit', () => ({ ...jest.requireActual('@nx/devkit'), workspaceRoot: '/root', @@ -981,8 +983,11 @@ describe('TargetProjectLocator', () => { }); describe('isBuiltinModuleImport()', () => { - const allBuiltinModules = require('node:module').builtinModules; - it.each(allBuiltinModules)( + const withExclusions = builtinModules.filter( + (s) => !['test/mock_loader'].includes(s) + ); + + it.each(withExclusions)( `should return true for %s builtin module`, (builtinModule) => { expect(isBuiltinModuleImport(builtinModule)).toBe(true);