Skip to content

Commit c770609

Browse files
authored
fix(@angular/cli): also clean the worktree-local cache in a git worktree
in git worktree `path` points to shared cache in main checkout, and since #33976 dev-server keeps its Vite cache in worktree own `.angular/cache`, so `ng cache clean` wiped cache of main checkout and left stale Vite prebundles of worktree in place `getCacheConfig` now also returns `localPath`, resolved against workspace like `localBasePath` in @angular/build, and clean removes both. Outside of worktree they are same path
1 parent ee847de commit c770609

4 files changed

Lines changed: 135 additions & 9 deletions

File tree

‎packages/angular/cli/src/commands/cache/clean/cli.ts‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,19 @@ export class CacheCleanModule extends CommandModule implements CommandModuleImpl
2424
return localYargs.strict();
2525
}
2626

27-
run(): Promise<void> {
28-
const { path } = getCacheConfig(this.context.workspace);
27+
async run(): Promise<void> {
28+
const { path, localPath } = getCacheConfig(this.context.workspace);
2929

30-
return rm(path, {
31-
force: true,
32-
recursive: true,
33-
maxRetries: 3,
34-
});
30+
// In a Git worktree, `path` is shared with the main checkout and `localPath` holds the
31+
// development server's Vite cache. Both are the same path outside of a worktree.
32+
await Promise.all(
33+
Array.from(new Set([path, localPath]), (cachePath) =>
34+
rm(cachePath, {
35+
force: true,
36+
recursive: true,
37+
maxRetries: 3,
38+
}),
39+
),
40+
);
3541
}
3642
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import type { workspaces } from '@angular-devkit/core';
10+
import assert from 'node:assert';
11+
import { existsSync } from 'node:fs';
12+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
13+
import { join } from 'node:path';
14+
import type { CommandContext } from '../../../command-builder/command-module';
15+
import type { AngularWorkspace } from '../../../utilities/config';
16+
import { CacheCleanModule } from './cli';
17+
18+
describe('CacheCleanModule', () => {
19+
let tempDir: string;
20+
21+
beforeEach(async () => {
22+
const baseTmpDir = process.env['TEST_TMPDIR'];
23+
assert(baseTmpDir, 'TEST_TMPDIR is not set');
24+
tempDir = await mkdtemp(join(baseTmpDir, 'angular-cli-cache-clean-spec-'));
25+
});
26+
27+
afterEach(async () => {
28+
await rm(tempDir, { recursive: true, force: true });
29+
});
30+
31+
function runClean(basePath: string, cliExtension?: unknown): Promise<void> {
32+
const workspace = {
33+
basePath,
34+
extensions: cliExtension ? { cli: cliExtension } : {},
35+
projects: {} as unknown as workspaces.ProjectDefinitionCollection,
36+
filePath: join(basePath, 'angular.json'),
37+
getCli: () => cliExtension,
38+
getProjectCli: () => undefined,
39+
save: () => Promise.resolve(),
40+
} as unknown as AngularWorkspace;
41+
42+
return new CacheCleanModule({ workspace } as unknown as CommandContext).run();
43+
}
44+
45+
it('should delete the cache of a standard repository', async () => {
46+
const workspaceRoot = join(tempDir, 'project');
47+
await mkdir(join(workspaceRoot, '.git'), { recursive: true });
48+
await mkdir(join(workspaceRoot, '.angular/cache/app'), { recursive: true });
49+
50+
await runClean(workspaceRoot);
51+
52+
expect(existsSync(join(workspaceRoot, '.angular/cache'))).toBeFalse();
53+
});
54+
55+
it('should delete both the shared and the worktree-local cache in a git worktree', async () => {
56+
const mainRepoRoot = join(tempDir, 'main-repo');
57+
const mainGitDir = join(mainRepoRoot, '.git');
58+
const worktreeRoot = join(tempDir, 'worktree');
59+
const worktreeMetadataDir = join(mainGitDir, 'worktrees/wt-1');
60+
61+
await mkdir(worktreeMetadataDir, { recursive: true });
62+
await mkdir(worktreeRoot, { recursive: true });
63+
await writeFile(join(worktreeRoot, '.git'), `gitdir: ${worktreeMetadataDir}`);
64+
await writeFile(join(worktreeMetadataDir, 'commondir'), '../..');
65+
66+
// The shared cache lives in the main checkout, while the development server keeps
67+
// its Vite cache in the worktree itself.
68+
await mkdir(join(mainRepoRoot, '.angular/cache/app'), { recursive: true });
69+
await mkdir(join(worktreeRoot, '.angular/cache/app/dev-server/vite'), { recursive: true });
70+
71+
await runClean(worktreeRoot);
72+
73+
expect(existsSync(join(mainRepoRoot, '.angular/cache'))).toBeFalse();
74+
expect(existsSync(join(worktreeRoot, '.angular/cache'))).toBeFalse();
75+
});
76+
77+
it('should succeed when cache directories do not exist', async () => {
78+
const workspaceRoot = join(tempDir, 'project');
79+
await mkdir(join(workspaceRoot, '.git'), { recursive: true });
80+
81+
await expectAsync(runClean(workspaceRoot)).toBeResolved();
82+
});
83+
84+
it('should delete both the shared and the worktree-local cache with a custom relative path', async () => {
85+
const mainRepoRoot = join(tempDir, 'main-repo');
86+
const mainGitDir = join(mainRepoRoot, '.git');
87+
const worktreeRoot = join(tempDir, 'worktree');
88+
const worktreeMetadataDir = join(mainGitDir, 'worktrees/wt-1');
89+
90+
await mkdir(worktreeMetadataDir, { recursive: true });
91+
await mkdir(worktreeRoot, { recursive: true });
92+
await writeFile(join(worktreeRoot, '.git'), `gitdir: ${worktreeMetadataDir}`);
93+
await writeFile(join(worktreeMetadataDir, 'commondir'), '../..');
94+
95+
const customCachePath = 'custom-cache-dir';
96+
await mkdir(join(mainRepoRoot, customCachePath, 'app'), { recursive: true });
97+
await mkdir(join(worktreeRoot, customCachePath, 'app/dev-server/vite'), { recursive: true });
98+
99+
await runClean(worktreeRoot, { cache: { path: customCachePath } });
100+
101+
expect(existsSync(join(mainRepoRoot, customCachePath))).toBeFalse();
102+
expect(existsSync(join(worktreeRoot, customCachePath))).toBeFalse();
103+
});
104+
});

‎packages/angular/cli/src/commands/cache/utilities.ts‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,22 @@ function getCacheBasePath(workspaceRoot: string, cachePathSetting: string): stri
6969
return resolve(workspaceRoot, cachePathSetting);
7070
}
7171

72-
export function getCacheConfig(workspace: AngularWorkspace | undefined): Required<Cache> {
72+
export interface CacheConfig extends Required<Cache> {
73+
/**
74+
* The cache path resolved against the workspace itself. In a Git worktree, `path` is shared
75+
* with the main checkout, while the development server keeps its Vite cache in this path.
76+
*/
77+
localPath: string;
78+
}
79+
80+
export function getCacheConfig(workspace: AngularWorkspace | undefined): CacheConfig {
7381
if (!workspace) {
7482
throw new Error(`Cannot retrieve cache configuration as workspace is not defined.`);
7583
}
7684

77-
const defaultSettings: Required<Cache> = {
85+
const defaultSettings: CacheConfig = {
7886
path: getCacheBasePath(workspace.basePath, '.angular/cache'),
87+
localPath: resolve(workspace.basePath, '.angular/cache'),
7988
environment: Environment.Local,
8089
enabled: true,
8190
};
@@ -99,6 +108,7 @@ export function getCacheConfig(workspace: AngularWorkspace | undefined): Require
99108

100109
return {
101110
path: getCacheBasePath(workspace.basePath, path),
111+
localPath: resolve(workspace.basePath, path),
102112
environment,
103113
enabled,
104114
};

‎packages/angular/cli/src/commands/cache/utilities_spec.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ describe('CLI cache config utilities', () => {
4545
const config = getCacheConfig(mockWorkspace(workspaceRoot));
4646

4747
expect(config.path).toBe(resolve(workspaceRoot, '.angular/cache'));
48+
expect(config.localPath).toBe(config.path);
4849
});
4950

5051
it('should resolve default cache path relative to main repository root in a git worktree', async () => {
@@ -67,6 +68,7 @@ describe('CLI cache config utilities', () => {
6768
const config = getCacheConfig(mockWorkspace(worktreeRoot));
6869

6970
expect(config.path).toBe(resolve(mainRepoRoot, '.angular/cache'));
71+
expect(config.localPath).toBe(resolve(worktreeRoot, '.angular/cache'));
7072
});
7173

7274
it('should resolve default cache path relative to corresponding nested workspace in main repository for a git worktree', async () => {
@@ -92,6 +94,7 @@ describe('CLI cache config utilities', () => {
9294
const config = getCacheConfig(mockWorkspace(worktreeWorkspaceRoot));
9395

9496
expect(config.path).toBe(resolve(mainWorkspaceRoot, '.angular/cache'));
97+
expect(config.localPath).toBe(resolve(worktreeWorkspaceRoot, '.angular/cache'));
9598
});
9699

97100
it('should fall back to worktree workspace basePath when nested workspace does not exist in main repository', async () => {
@@ -113,6 +116,7 @@ describe('CLI cache config utilities', () => {
113116
const config = getCacheConfig(mockWorkspace(worktreeWorkspaceRoot));
114117

115118
expect(config.path).toBe(resolve(worktreeWorkspaceRoot, '.angular/cache'));
119+
expect(config.localPath).toBe(config.path);
116120
});
117121

118122
it('should resolve custom relative cache path relative to main repository root in a git worktree', async () => {
@@ -137,6 +141,7 @@ describe('CLI cache config utilities', () => {
137141
);
138142

139143
expect(config.path).toBe(resolve(mainRepoRoot, 'custom/cache-dir'));
144+
expect(config.localPath).toBe(resolve(worktreeRoot, 'custom/cache-dir'));
140145
});
141146

142147
it('should resolve cache path relative to workspace basePath in a git submodule', async () => {
@@ -155,5 +160,6 @@ describe('CLI cache config utilities', () => {
155160
const config = getCacheConfig(mockWorkspace(submoduleRoot));
156161

157162
expect(config.path).toBe(resolve(submoduleRoot, '.angular/cache'));
163+
expect(config.localPath).toBe(config.path);
158164
});
159165
});

0 commit comments

Comments
 (0)