Skip to content
Open
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
45 changes: 43 additions & 2 deletions packages/angular/build/src/utils/normalize-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { join, resolve } from 'node:path';
import { existsSync, readFileSync, statSync } from 'node:fs';
import { dirname, isAbsolute, join, resolve } from 'node:path';

/** Version placeholder is replaced during the build process with actual package version */
const VERSION = '0.0.0-PLACEHOLDER';
Expand Down Expand Up @@ -39,6 +40,46 @@ function hasCacheMetadata(value: unknown): value is { cli: { cache: CacheMetadat
);
}

function getCacheBasePath(workspaceRoot: string, cachePathSetting: string): string {
if (isAbsolute(cachePathSetting)) {
return cachePathSetting;
}

try {
// Find the git directory, walking up from workspaceRoot if necessary
let currentDir = workspaceRoot;
while (true) {
const gitPath = join(currentDir, '.git');
if (existsSync(gitPath)) {
const stat = statSync(gitPath);
if (stat.isFile()) {
// Could be a git worktree (or submodule)
const content = readFileSync(gitPath, 'utf8');
const match = /^gitdir:\s*(.+)$/m.exec(content);
if (match) {
const gitdir = resolve(currentDir, match[1].trim());
const commondirPath = join(gitdir, 'commondir');
if (existsSync(commondirPath)) {
// It's a git worktree
const commondir = readFileSync(commondirPath, 'utf8').trim();
const commonGitDir = resolve(gitdir, commondir);

return resolve(dirname(commonGitDir), cachePathSetting);
}
}
}
}
const parentDir = dirname(currentDir);
if (parentDir === currentDir) {
break;
}
currentDir = parentDir;
}
} catch {}
Comment thread
clydin marked this conversation as resolved.

return resolve(workspaceRoot, cachePathSetting);
}

export function normalizeCacheOptions(
projectMetadata: unknown,
worspaceRoot: string,
Expand All @@ -65,7 +106,7 @@ export function normalizeCacheOptions(
}
}

const cacheBasePath = resolve(worspaceRoot, path);
const cacheBasePath = getCacheBasePath(worspaceRoot, path);

return {
enabled: cacheEnabled,
Expand Down
82 changes: 82 additions & 0 deletions packages/angular/build/src/utils/normalize-cache_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* @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 { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { normalizeCacheOptions } from './normalize-cache';

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

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'angular-cache-spec-'));
});

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

it('should resolve cache path relative to workspace root in a standard repository', async () => {
const workspaceRoot = join(tempDir, 'project');
await mkdir(join(workspaceRoot, '.git'), { recursive: true });

const options = normalizeCacheOptions({}, workspaceRoot);

expect(options.basePath).toBe(resolve(workspaceRoot, '.angular/cache'));
});

it('should resolve cache path relative to main repository root in a git worktree', async () => {
const mainRepoRoot = join(tempDir, 'main-repo');
const mainGitDir = join(mainRepoRoot, '.git');
const worktreeRoot = join(tempDir, 'worktree');

// Create main repo structure
await mkdir(mainGitDir, { recursive: true });

// Create worktree folder and .git file pointing to the main repo's worktree metadata folder
const worktreeMetadataDir = join(mainGitDir, 'worktrees/wt-1');
await mkdir(worktreeMetadataDir, { recursive: true });
await mkdir(worktreeRoot, { recursive: true });
await writeFile(join(worktreeRoot, '.git'), `gitdir: ${worktreeMetadataDir}`);

// Create the commondir file in the worktree metadata folder pointing back to the main .git dir
await writeFile(join(worktreeMetadataDir, 'commondir'), '../..');

const options = normalizeCacheOptions({}, worktreeRoot);

expect(options.basePath).toBe(resolve(mainRepoRoot, '.angular/cache'));
});

it('should resolve cache path relative to workspace root in a git submodule', async () => {
const mainRepoRoot = join(tempDir, 'main-repo');
const submoduleRoot = join(mainRepoRoot, 'submodule');

// Create main repo structure and submodule metadata folder
const submoduleGitDir = join(mainRepoRoot, '.git/modules/sub');
await mkdir(submoduleGitDir, { recursive: true });
await mkdir(submoduleRoot, { recursive: true });

// Create .git file in submodule pointing to the metadata folder
await writeFile(join(submoduleRoot, '.git'), `gitdir: ../.git/modules/sub`);

// Submodules do NOT have a 'commondir' file.
const options = normalizeCacheOptions({}, submoduleRoot);

expect(options.basePath).toBe(resolve(submoduleRoot, '.angular/cache'));
});

it('should resolve cache path relative to workspace root when there is no git repository', async () => {
const workspaceRoot = join(tempDir, 'project');
await mkdir(workspaceRoot, { recursive: true });

const options = normalizeCacheOptions({}, workspaceRoot);

expect(options.basePath).toBe(resolve(workspaceRoot, '.angular/cache'));
});
});
49 changes: 45 additions & 4 deletions packages/angular/cli/src/commands/cache/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
*/

import { isJsonObject } from '@angular-devkit/core';
import { resolve } from 'node:path';
import { existsSync, readFileSync, statSync } from 'node:fs';
import { dirname, isAbsolute, join, resolve } from 'node:path';
import { Cache, Environment } from '../../../lib/config/workspace-schema';
import { AngularWorkspace } from '../../utilities/config';

Expand All @@ -23,13 +24,53 @@ export function updateCacheConfig<K extends keyof Cache>(
return workspace.save();
}

function getCacheBasePath(workspaceRoot: string, cachePathSetting: string): string {
if (isAbsolute(cachePathSetting)) {
return cachePathSetting;
}

try {
// Find the git directory, walking up from workspaceRoot if necessary
let currentDir = workspaceRoot;
while (true) {
const gitPath = join(currentDir, '.git');
if (existsSync(gitPath)) {
const stat = statSync(gitPath);
if (stat.isFile()) {
// Could be a git worktree (or submodule)
const content = readFileSync(gitPath, 'utf8');
const match = /^gitdir:\s*(.+)$/m.exec(content);
if (match) {
const gitdir = resolve(currentDir, match[1].trim());
const commondirPath = join(gitdir, 'commondir');
if (existsSync(commondirPath)) {
// It's a git worktree
const commondir = readFileSync(commondirPath, 'utf8').trim();
const commonGitDir = resolve(gitdir, commondir);

return resolve(dirname(commonGitDir), cachePathSetting);
}
}
}
}
const parentDir = dirname(currentDir);
if (parentDir === currentDir) {
break;
}
currentDir = parentDir;
}
} catch {}
Comment thread
clydin marked this conversation as resolved.

return resolve(workspaceRoot, cachePathSetting);
}

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

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

const {
path = defaultSettings.path,
path = '.angular/cache',
environment = defaultSettings.environment,
enabled = defaultSettings.enabled,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} = cacheSettings as Record<string, any>;

return {
path: resolve(workspace.basePath, path),
path: getCacheBasePath(workspace.basePath, path),
environment,
enabled,
};
Expand Down
111 changes: 111 additions & 0 deletions packages/angular/cli/src/commands/cache/utilities_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* @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 { workspaces } from '@angular-devkit/core';
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { AngularWorkspace } from '../../utilities/config';
import { getCacheConfig } from './utilities';

describe('CLI cache config utilities', () => {
let tempDir: string;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'angular-cli-cache-spec-'));
});

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

function mockWorkspace(basePath: string, cliExtension?: unknown): AngularWorkspace {
return {
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;
}

it('should resolve default cache path relative to workspace basePath in a standard repository', async () => {
const workspaceRoot = join(tempDir, 'project');
await mkdir(join(workspaceRoot, '.git'), { recursive: true });

const config = getCacheConfig(mockWorkspace(workspaceRoot));

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

it('should resolve default cache path relative to main repository root in a git worktree', async () => {
const mainRepoRoot = join(tempDir, 'main-repo');
const mainGitDir = join(mainRepoRoot, '.git');
const worktreeRoot = join(tempDir, 'worktree');

// Create main repo structure
await mkdir(mainGitDir, { recursive: true });

// Create worktree folder and .git file pointing to the main repo's worktree metadata folder
const worktreeMetadataDir = join(mainGitDir, 'worktrees/wt-1');
await mkdir(worktreeMetadataDir, { recursive: true });
await mkdir(worktreeRoot, { recursive: true });
await writeFile(join(worktreeRoot, '.git'), `gitdir: ${worktreeMetadataDir}`);

// Create the commondir file in the worktree metadata folder pointing back to the main .git dir
await writeFile(join(worktreeMetadataDir, 'commondir'), '../..');

const config = getCacheConfig(mockWorkspace(worktreeRoot));

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

it('should resolve custom relative cache path relative to main repository root in a git worktree', async () => {
const mainRepoRoot = join(tempDir, 'main-repo');
const mainGitDir = join(mainRepoRoot, '.git');
const worktreeRoot = join(tempDir, 'worktree');

// Create main repo structure
await mkdir(mainGitDir, { recursive: true });

// Create worktree folder and .git file pointing to the main repo's worktree metadata folder
const worktreeMetadataDir = join(mainGitDir, 'worktrees/wt-1');
await mkdir(worktreeMetadataDir, { recursive: true });
await mkdir(worktreeRoot, { recursive: true });
await writeFile(join(worktreeRoot, '.git'), `gitdir: ${worktreeMetadataDir}`);

// Create the commondir file in the worktree metadata folder pointing back to the main .git dir
await writeFile(join(worktreeMetadataDir, 'commondir'), '../..');

const config = getCacheConfig(
mockWorkspace(worktreeRoot, { cache: { path: 'custom/cache-dir' } }),
);

expect(config.path).toBe(resolve(mainRepoRoot, 'custom/cache-dir'));
});

it('should resolve cache path relative to workspace basePath in a git submodule', async () => {
const mainRepoRoot = join(tempDir, 'main-repo');
const submoduleRoot = join(mainRepoRoot, 'submodule');

// Create main repo structure and submodule metadata folder
const submoduleGitDir = join(mainRepoRoot, '.git/modules/sub');
await mkdir(submoduleGitDir, { recursive: true });
await mkdir(submoduleRoot, { recursive: true });

// Create .git file in submodule pointing to the metadata folder
await writeFile(join(submoduleRoot, '.git'), `gitdir: ../.git/modules/sub`);

// Submodules do NOT have a 'commondir' file.
const config = getCacheConfig(mockWorkspace(submoduleRoot));

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