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
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ export async function* runEsBuildBuildAction(
`${toPosixPath(workspaceRoot)}/**/.*/**`,
];

if (cacheOptions.localBasePath && cacheOptions.localBasePath !== cacheOptions.basePath) {
const normalizedLocalCacheBase = toPosixPath(cacheOptions.localBasePath);
ignored.push(normalizedLocalCacheBase, `${normalizedLocalCacheBase}/**`);
}

// Setup a watcher
const { createWatcher } = await import('../../tools/esbuild/watcher');
watcher = await createWatcher({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,11 @@ export async function setupServer(
*/
const preTransformRequests =
externalMetadata.explicitBrowser.length === 0 && ssrMode === ServerSsrMode.NoSsr;
const cacheDir = join(serverOptions.cacheOptions.path, serverOptions.buildTarget.project, 'vite');
const cacheDir = join(
serverOptions.cacheOptions.localPath ?? serverOptions.cacheOptions.path,
serverOptions.buildTarget.project,
'vite',
);

const configuration: Vite.InlineConfig = {
configFile: false,
Expand Down
19 changes: 17 additions & 2 deletions packages/angular/build/src/utils/normalize-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ export interface NormalizedCachedOptions {

/** Disk cache base path. Example: `/.angular/cache`. */
basePath: string;

/**
* Workspace-local disk cache path. Example: `/.angular/cache/v12.0.0`.
* Always resolves relative to the current workspace root, even within a Git worktree.
*/
localPath?: string;

/**
* Workspace-local disk cache base path. Example: `/.angular/cache`.
* Always resolves relative to the current workspace root, even within a Git worktree.
*/
localBasePath?: string;
}

interface CacheMetadata {
Expand Down Expand Up @@ -82,7 +94,7 @@ function getCacheBasePath(workspaceRoot: string, cachePathSetting: string): stri

export function normalizeCacheOptions(
projectMetadata: unknown,
worspaceRoot: string,
workspaceRoot: string,
): NormalizedCachedOptions {
const cacheMetadata = hasCacheMetadata(projectMetadata) ? projectMetadata.cli.cache : {};

Expand All @@ -106,11 +118,14 @@ export function normalizeCacheOptions(
}
}

const cacheBasePath = getCacheBasePath(worspaceRoot, path);
const cacheBasePath = getCacheBasePath(workspaceRoot, path);
const localCacheBasePath = isAbsolute(path) ? path : resolve(workspaceRoot, path);

return {
enabled: cacheEnabled,
basePath: cacheBasePath,
path: join(cacheBasePath, VERSION),
localBasePath: localCacheBasePath,
localPath: join(localCacheBasePath, VERSION),
};
}
58 changes: 58 additions & 0 deletions packages/angular/build/src/utils/normalize-cache_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ describe('normalizeCacheOptions', () => {
const options = normalizeCacheOptions({}, workspaceRoot);

expect(options.basePath).toBe(resolve(workspaceRoot, '.angular/cache'));
expect(options.localBasePath).toBe(resolve(workspaceRoot, '.angular/cache'));
expect(options.localPath).toBe(resolve(workspaceRoot, '.angular/cache', '0.0.0-PLACEHOLDER'));
});

it('should resolve cache path relative to main repository root in a git worktree', async () => {
Expand All @@ -51,6 +53,58 @@ describe('normalizeCacheOptions', () => {
const options = normalizeCacheOptions({}, worktreeRoot);

expect(options.basePath).toBe(resolve(mainRepoRoot, '.angular/cache'));
expect(options.path).toBe(resolve(mainRepoRoot, '.angular/cache', '0.0.0-PLACEHOLDER'));
expect(options.localBasePath).toBe(resolve(worktreeRoot, '.angular/cache'));
expect(options.localPath).toBe(resolve(worktreeRoot, '.angular/cache', '0.0.0-PLACEHOLDER'));
});

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

await mkdir(mainGitDir, { recursive: true });

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 options = normalizeCacheOptions(
{ cli: { cache: { path: 'custom-cache' } } },
worktreeRoot,
);

expect(options.basePath).toBe(resolve(mainRepoRoot, 'custom-cache'));
expect(options.path).toBe(resolve(mainRepoRoot, 'custom-cache', '0.0.0-PLACEHOLDER'));
expect(options.localBasePath).toBe(resolve(worktreeRoot, 'custom-cache'));
expect(options.localPath).toBe(resolve(worktreeRoot, 'custom-cache', '0.0.0-PLACEHOLDER'));
});

it('should preserve absolute cache path for both shared and local paths', async () => {
const mainRepoRoot = join(tempDir, 'main-repo');
const mainGitDir = join(mainRepoRoot, '.git');
const worktreeRoot = join(tempDir, 'worktree');
const absoluteCachePath = join(tempDir, 'absolute-cache');

await mkdir(mainGitDir, { recursive: true });

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 options = normalizeCacheOptions(
{ cli: { cache: { path: absoluteCachePath } } },
worktreeRoot,
);

expect(options.basePath).toBe(absoluteCachePath);
expect(options.path).toBe(resolve(absoluteCachePath, '0.0.0-PLACEHOLDER'));
expect(options.localBasePath).toBe(absoluteCachePath);
expect(options.localPath).toBe(resolve(absoluteCachePath, '0.0.0-PLACEHOLDER'));
});

it('should resolve cache path relative to workspace root in a git submodule', async () => {
Expand All @@ -69,6 +123,8 @@ describe('normalizeCacheOptions', () => {
const options = normalizeCacheOptions({}, submoduleRoot);

expect(options.basePath).toBe(resolve(submoduleRoot, '.angular/cache'));
expect(options.localBasePath).toBe(resolve(submoduleRoot, '.angular/cache'));
expect(options.localPath).toBe(resolve(submoduleRoot, '.angular/cache', '0.0.0-PLACEHOLDER'));
});

it('should resolve cache path relative to workspace root when there is no git repository', async () => {
Expand All @@ -78,5 +134,7 @@ describe('normalizeCacheOptions', () => {
const options = normalizeCacheOptions({}, workspaceRoot);

expect(options.basePath).toBe(resolve(workspaceRoot, '.angular/cache'));
expect(options.localBasePath).toBe(resolve(workspaceRoot, '.angular/cache'));
expect(options.localPath).toBe(resolve(workspaceRoot, '.angular/cache', '0.0.0-PLACEHOLDER'));
});
});
42 changes: 29 additions & 13 deletions packages/angular/build/src/utils/purge-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,41 @@ export async function purgeStaleBuildCache(context: BuilderContext): Promise<voi
}

const metadata = await context.getProjectMetadata(projectName);
const { basePath, path, enabled } = normalizeCacheOptions(metadata, context.workspaceRoot);
const { basePath, path, localBasePath, localPath, enabled } = normalizeCacheOptions(
metadata,
context.workspaceRoot,
);

if (!enabled) {
return;
}

let baseEntries;
try {
baseEntries = await readdir(basePath, { withFileTypes: true });
} catch {
// No purging possible if base path does not exist or cannot otherwise be accessed
return;
const basePaths = new Set([basePath]);
if (localBasePath) {
basePaths.add(localBasePath);
}

const entriesToDelete = baseEntries
.filter((d) => d.isDirectory())
.map((d) => join(basePath, d.name))
.filter((cachePath) => cachePath !== path)
.map((stalePath) => rm(stalePath, { force: true, recursive: true, maxRetries: 3 }));
for (const base of basePaths) {
let baseEntries;
try {
baseEntries = await readdir(base, { withFileTypes: true });
} catch {
// No purging possible if base path does not exist or cannot otherwise be accessed
continue;
}

const currentPath = base === localBasePath ? localPath : path;
if (!currentPath) {
// Avoid purging if current path is unavailable to prevent deleting the active cache
continue;
}

await Promise.allSettled(entriesToDelete);
const entriesToDelete = baseEntries
.filter((d) => d.isDirectory())
.map((d) => join(base, d.name))
.filter((cachePath) => cachePath !== currentPath)
.map((stalePath) => rm(stalePath, { force: true, recursive: true, maxRetries: 3 }));

await Promise.allSettled(entriesToDelete);
}
}