Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions packages/angular/cli/src/commands/cache/clean/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,19 @@ export class CacheCleanModule extends CommandModule implements CommandModuleImpl
return localYargs.strict();
}

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

return rm(path, {
force: true,
recursive: true,
maxRetries: 3,
});
// In a Git worktree, `path` is shared with the main checkout and `localPath` holds the
// development server's Vite cache. Both are the same path outside of a worktree.
await Promise.all(
Array.from(new Set([path, localPath]), (cachePath) =>
rm(cachePath, {
force: true,
recursive: true,
maxRetries: 3,
}),
),
);
}
}
104 changes: 104 additions & 0 deletions packages/angular/cli/src/commands/cache/clean/cli_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import type { workspaces } from '@angular-devkit/core';
import assert from 'node:assert';
import { existsSync } from 'node:fs';
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import type { CommandContext } from '../../../command-builder/command-module';
import type { AngularWorkspace } from '../../../utilities/config';
import { CacheCleanModule } from './cli';

describe('CacheCleanModule', () => {
let tempDir: string;

beforeEach(async () => {
const baseTmpDir = process.env['TEST_TMPDIR'];
assert(baseTmpDir, 'TEST_TMPDIR is not set');
tempDir = await mkdtemp(join(baseTmpDir, 'angular-cli-cache-clean-spec-'));
});

afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});

function runClean(basePath: string, cliExtension?: unknown): Promise<void> {
const workspace = {
basePath,
extensions: cliExtension ? { cli: cliExtension } : {},
projects: {} as unknown as workspaces.ProjectDefinitionCollection,
filePath: join(basePath, 'angular.json'),
getCli: () => cliExtension,
getProjectCli: () => undefined,
save: () => Promise.resolve(),
} as unknown as AngularWorkspace;

return new CacheCleanModule({ workspace } as unknown as CommandContext).run();
}

it('should delete the cache of a standard repository', async () => {
const workspaceRoot = join(tempDir, 'project');
await mkdir(join(workspaceRoot, '.git'), { recursive: true });
await mkdir(join(workspaceRoot, '.angular/cache/app'), { recursive: true });

await runClean(workspaceRoot);

expect(existsSync(join(workspaceRoot, '.angular/cache'))).toBeFalse();
});

it('should delete both the shared and the worktree-local cache in a git worktree', async () => {
const mainRepoRoot = join(tempDir, 'main-repo');
const mainGitDir = join(mainRepoRoot, '.git');
const worktreeRoot = join(tempDir, 'worktree');
const worktreeMetadataDir = join(mainGitDir, 'worktrees/wt-1');

await mkdir(worktreeMetadataDir, { recursive: true });
await mkdir(worktreeRoot, { recursive: true });
await writeFile(join(worktreeRoot, '.git'), `gitdir: ${worktreeMetadataDir}`);
await writeFile(join(worktreeMetadataDir, 'commondir'), '../..');

// The shared cache lives in the main checkout, while the development server keeps
// its Vite cache in the worktree itself.
await mkdir(join(mainRepoRoot, '.angular/cache/app'), { recursive: true });
await mkdir(join(worktreeRoot, '.angular/cache/app/dev-server/vite'), { recursive: true });

await runClean(worktreeRoot);

expect(existsSync(join(mainRepoRoot, '.angular/cache'))).toBeFalse();
expect(existsSync(join(worktreeRoot, '.angular/cache'))).toBeFalse();
Comment thread
thekhegay marked this conversation as resolved.
Resolved
});

it('should succeed when cache directories do not exist', async () => {
const workspaceRoot = join(tempDir, 'project');
await mkdir(join(workspaceRoot, '.git'), { recursive: true });

await expectAsync(runClean(workspaceRoot)).toBeResolved();
});

it('should delete both the shared and the worktree-local cache with a custom relative path', async () => {
const mainRepoRoot = join(tempDir, 'main-repo');
const mainGitDir = join(mainRepoRoot, '.git');
const worktreeRoot = join(tempDir, 'worktree');
const worktreeMetadataDir = join(mainGitDir, 'worktrees/wt-1');

await mkdir(worktreeMetadataDir, { recursive: true });
await mkdir(worktreeRoot, { recursive: true });
await writeFile(join(worktreeRoot, '.git'), `gitdir: ${worktreeMetadataDir}`);
await writeFile(join(worktreeMetadataDir, 'commondir'), '../..');

const customCachePath = 'custom-cache-dir';
await mkdir(join(mainRepoRoot, customCachePath, 'app'), { recursive: true });
await mkdir(join(worktreeRoot, customCachePath, 'app/dev-server/vite'), { recursive: true });

await runClean(worktreeRoot, { cache: { path: customCachePath } });

expect(existsSync(join(mainRepoRoot, customCachePath))).toBeFalse();
expect(existsSync(join(worktreeRoot, customCachePath))).toBeFalse();
});
});
14 changes: 12 additions & 2 deletions packages/angular/cli/src/commands/cache/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,22 @@ function getCacheBasePath(workspaceRoot: string, cachePathSetting: string): stri
return resolve(workspaceRoot, cachePathSetting);
}

export function getCacheConfig(workspace: AngularWorkspace | undefined): Required<Cache> {
export interface CacheConfig extends Required<Cache> {
/**
* The cache path resolved against the workspace itself. In a Git worktree, `path` is shared
* with the main checkout, while the development server keeps its Vite cache in this path.
*/
localPath: string;
}

export function getCacheConfig(workspace: AngularWorkspace | undefined): CacheConfig {
if (!workspace) {
throw new Error(`Cannot retrieve cache configuration as workspace is not defined.`);
}

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

return {
path: getCacheBasePath(workspace.basePath, path),
localPath: resolve(workspace.basePath, path),
environment,
enabled,
};
Expand Down
6 changes: 6 additions & 0 deletions packages/angular/cli/src/commands/cache/utilities_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe('CLI cache config utilities', () => {
const config = getCacheConfig(mockWorkspace(workspaceRoot));

expect(config.path).toBe(resolve(workspaceRoot, '.angular/cache'));
expect(config.localPath).toBe(config.path);
});

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

expect(config.path).toBe(resolve(mainRepoRoot, '.angular/cache'));
expect(config.localPath).toBe(resolve(worktreeRoot, '.angular/cache'));
});

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

expect(config.path).toBe(resolve(mainWorkspaceRoot, '.angular/cache'));
expect(config.localPath).toBe(resolve(worktreeWorkspaceRoot, '.angular/cache'));
});

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

expect(config.path).toBe(resolve(worktreeWorkspaceRoot, '.angular/cache'));
expect(config.localPath).toBe(config.path);
});

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

expect(config.path).toBe(resolve(mainRepoRoot, 'custom/cache-dir'));
expect(config.localPath).toBe(resolve(worktreeRoot, 'custom/cache-dir'));
Comment thread
thekhegay marked this conversation as resolved.
Resolved
});

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

expect(config.path).toBe(resolve(submoduleRoot, '.angular/cache'));
expect(config.localPath).toBe(config.path);
});
});
Loading