Skip to content

Commit a0be6ad

Browse files
authored
fix(core): track all task outputs regardless of path depth (#34321)
## Current Behavior When a task has outputs at different path depths, some outputs may not be tracked. This causes: - Deleted output files not being detected - Cache restoration being skipped with message "existing outputs match the cache, left as is" - Files not being restored even though they exist in cache ## Expected Behavior All task outputs are tracked regardless of their path depth, ensuring: - Deleted outputs are correctly detected - Cache restoration happens when outputs are missing
1 parent dffdfa6 commit a0be6ad

2 files changed

Lines changed: 79 additions & 12 deletions

File tree

packages/nx/src/utils/collapse-expanded-outputs.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,36 @@ describe('collapseExpandedOutputs', () => {
101101

102102
expect(res).toEqual(['dist/apps/app1']);
103103
});
104+
105+
it('should preserve shallow paths when deep paths cause collapse', () => {
106+
const outputs = [
107+
'dist/libs/mylib/esm/src/file1.js',
108+
'dist/libs/mylib/esm/src/file2.js',
109+
'dist/libs/mylib/esm/src/file3.js',
110+
'dist/libs/mylib/esm/src/file4.js',
111+
'libs/mylib/src/generated.ts',
112+
];
113+
const res = collapseExpandedOutputs(outputs);
114+
115+
expect(res).toContain('dist/libs/mylib/esm/src');
116+
expect(res).toContain('libs/mylib/src/generated.ts');
117+
expect(res).toHaveLength(2);
118+
});
119+
120+
it('should preserve multiple shallow paths at different depths', () => {
121+
const outputs = [
122+
'dist/deep/nested/path/a.js',
123+
'dist/deep/nested/path/b.js',
124+
'dist/deep/nested/path/c.js',
125+
'dist/deep/nested/path/d.js',
126+
'shallow/file.txt',
127+
'medium/depth/file.txt',
128+
];
129+
const res = collapseExpandedOutputs(outputs);
130+
131+
expect(res).toContain('dist/deep/nested/path');
132+
expect(res).toContain('shallow/file.txt');
133+
expect(res).toContain('medium/depth/file.txt');
134+
expect(res).toHaveLength(3);
135+
});
104136
});

packages/nx/src/utils/collapse-expanded-outputs.ts

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,21 @@ import { dirname } from 'path';
55
*/
66
const MAX_OUTPUTS_TO_CHECK_HASHES = 3;
77

8+
/**
9+
* Collapses a list of file paths into a smaller set of parent directories
10+
* when there are too many outputs to efficiently track individually.
11+
*
12+
* This prevents creating too many hash files by:
13+
* 1. Building a tree structure of all path components
14+
* 2. Finding the "collapse level" - the deepest level before exceeding MAX_OUTPUTS_TO_CHECK_HASHES
15+
* 3. Returning parent paths at/above collapse level, while preserving leaf paths that terminate early
16+
*/
817
export function collapseExpandedOutputs(expandedOutputs: string[]) {
18+
// Small output sets don't need collapsing
19+
if (expandedOutputs.length <= MAX_OUTPUTS_TO_CHECK_HASHES) {
20+
return [...expandedOutputs];
21+
}
22+
923
const tree: Set<string>[] = [];
1024

1125
// Create a Tree of directories/files
@@ -25,21 +39,42 @@ export function collapseExpandedOutputs(expandedOutputs: string[]) {
2539
}
2640
}
2741

28-
// Find a level in the tree that has too many outputs
29-
if (tree.length === 0) {
30-
return [];
42+
// Find collapse level: the level before the first level with too many outputs
43+
let collapseLevel = tree.length - 1;
44+
for (let j = 0; j < tree.length; j++) {
45+
if (tree[j].size > MAX_OUTPUTS_TO_CHECK_HASHES) {
46+
collapseLevel = Math.max(0, j - 1);
47+
break;
48+
}
49+
}
50+
51+
// Collect paths, preserving leaf paths that terminate before collapse level
52+
const result = new Set<string>();
53+
54+
// Convert sets to arrays once for faster iteration
55+
const levelArrays: string[][] = [];
56+
for (let i = 0; i <= collapseLevel + 1 && i < tree.length; i++) {
57+
levelArrays[i] = Array.from(tree[i]);
3158
}
3259

33-
let j = 0;
34-
let level = tree[j];
35-
for (j = 0; j < tree.length; j++) {
36-
level = tree[j];
37-
if (level.size > MAX_OUTPUTS_TO_CHECK_HASHES) {
38-
break;
60+
for (let level = 0; level <= collapseLevel; level++) {
61+
const nextLevelArray = levelArrays[level + 1];
62+
for (const path of levelArrays[level]) {
63+
let hasChildren = false;
64+
if (nextLevelArray) {
65+
for (const child of nextLevelArray) {
66+
if (child.startsWith(path + '/')) {
67+
hasChildren = true;
68+
break;
69+
}
70+
}
71+
}
72+
// Include path if it's at collapse level or doesn't have children (leaf)
73+
if (level === collapseLevel || !hasChildren) {
74+
result.add(path);
75+
}
3976
}
4077
}
4178

42-
// Return the level before the level with too many outputs
43-
// If the first level has too many outputs, return that one.
44-
return Array.from(tree[Math.max(0, j - 1)]);
79+
return Array.from(result);
4580
}

0 commit comments

Comments
 (0)