From 431414e6c54089b8ca62e2a24654bc86bfce6a93 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 2 Oct 2026 09:38:14 +0000 Subject: [PATCH 1/2] fix(@angular/build): scope cache directory by project and builder name Accept optional `projectName` and `builderName` parameters in `normalizeCacheOptions` to consistently scope `path` and `localPath` per project and builder across the application, dev-server, library, ng-packagr, and unit-test builders. Without scoping the cache path by `builderName`, builders that invoke the application build pipeline (such as `unit-test` during `ng test` and `application` during `ng build`) share the same project cache directory and overwrite shared cache artifacts such as `.tsbuildinfo` files, preventing incremental caching from working properly. --- .../build/src/builders/application/options.ts | 8 +- .../build/src/builders/dev-server/options.ts | 7 +- .../src/builders/dev-server/vite/server.ts | 6 +- .../build/src/builders/library/options.ts | 7 +- .../build/src/builders/ng-packagr/builder.ts | 2 + .../build/src/builders/unit-test/options.ts | 8 +- .../build/src/utils/normalize-cache.ts | 19 ++++- .../build/src/utils/normalize-cache_spec.ts | 78 +++++++++++++++++++ 8 files changed, 122 insertions(+), 13 deletions(-) diff --git a/packages/angular/build/src/builders/application/options.ts b/packages/angular/build/src/builders/application/options.ts index f7e60f4cc029..322c2dbef897 100644 --- a/packages/angular/build/src/builders/application/options.ts +++ b/packages/angular/build/src/builders/application/options.ts @@ -182,8 +182,12 @@ export async function normalizeOptions( const { projectRoot, projectSourceRoot } = getProjectRootPaths(workspaceRoot, projectMetadata); // Gather persistent caching option and provide a project specific cache location - const cacheOptions = normalizeCacheOptions(projectMetadata, workspaceRoot); - cacheOptions.path = path.join(cacheOptions.path, projectName); + const cacheOptions = normalizeCacheOptions( + projectMetadata, + workspaceRoot, + projectName, + context.builder.builderName, + ); const i18nOptions: I18nOptions & { duplicateTranslationBehavior?: I18NTranslation; diff --git a/packages/angular/build/src/builders/dev-server/options.ts b/packages/angular/build/src/builders/dev-server/options.ts index 7dfdc56a371d..bc63c69540f7 100644 --- a/packages/angular/build/src/builders/dev-server/options.ts +++ b/packages/angular/build/src/builders/dev-server/options.ts @@ -39,7 +39,12 @@ export async function normalizeOptions( const projectMetadata = await context.getProjectMetadata(projectName); const projectRoot = path.join(workspaceRoot, (projectMetadata.root as string | undefined) ?? ''); - const cacheOptions = normalizeCacheOptions(projectMetadata, workspaceRoot); + const cacheOptions = normalizeCacheOptions( + projectMetadata, + workspaceRoot, + projectName, + context.builder.builderName, + ); // Target specifier defaults to the current project's build target using a development configuration const buildTargetSpecifier = options.buildTarget ?? `::development`; diff --git a/packages/angular/build/src/builders/dev-server/vite/server.ts b/packages/angular/build/src/builders/dev-server/vite/server.ts index fb7870bd2d27..1564099480b4 100644 --- a/packages/angular/build/src/builders/dev-server/vite/server.ts +++ b/packages/angular/build/src/builders/dev-server/vite/server.ts @@ -165,11 +165,7 @@ export async function setupServer( */ const preTransformRequests = externalMetadata.explicitBrowser.length === 0 && ssrMode === ServerSsrMode.NoSsr; - const cacheDir = join( - serverOptions.cacheOptions.localPath ?? serverOptions.cacheOptions.path, - serverOptions.buildTarget.project, - 'vite', - ); + const cacheDir = serverOptions.cacheOptions.localPath ?? serverOptions.cacheOptions.path; const configuration: Vite.InlineConfig = { configFile: false, diff --git a/packages/angular/build/src/builders/library/options.ts b/packages/angular/build/src/builders/library/options.ts index fdf5354f7fa7..22179961a597 100644 --- a/packages/angular/build/src/builders/library/options.ts +++ b/packages/angular/build/src/builders/library/options.ts @@ -126,7 +126,12 @@ export async function normalizeLibraryOptions( projectSourceRoot, ); - const cacheOptions = normalizeCacheOptions(projectMetadata, workspaceRoot); + const cacheOptions = normalizeCacheOptions( + projectMetadata, + workspaceRoot, + projectName, + context.builder.builderName, + ); const styleIncludePaths = (stylePreprocessorOptions?.includePaths ?? []).map((p: string) => path.resolve(workspaceRoot, p), diff --git a/packages/angular/build/src/builders/ng-packagr/builder.ts b/packages/angular/build/src/builders/ng-packagr/builder.ts index 24b606aeede1..ea416ff5edc8 100644 --- a/packages/angular/build/src/builders/ng-packagr/builder.ts +++ b/packages/angular/build/src/builders/ng-packagr/builder.ts @@ -66,6 +66,8 @@ export async function* execute( const { enabled: cacheEnabled, path: cacheDirectory } = normalizeCacheOptions( metadata, context.workspaceRoot, + projectName, + context.builder.builderName, ); const ngPackagrOptions: NgPackagrOptions = { diff --git a/packages/angular/build/src/builders/unit-test/options.ts b/packages/angular/build/src/builders/unit-test/options.ts index 5328a5861219..f841670c072f 100644 --- a/packages/angular/build/src/builders/unit-test/options.ts +++ b/packages/angular/build/src/builders/unit-test/options.ts @@ -64,8 +64,12 @@ export async function normalizeOptions( const { projectRoot, projectSourceRoot } = getProjectRootPaths(workspaceRoot, projectMetadata); // Gather persistent caching option and provide a project specific cache location - const cacheOptions = normalizeCacheOptions(projectMetadata, workspaceRoot); - cacheOptions.path = path.join(cacheOptions.path, projectName); + const cacheOptions = normalizeCacheOptions( + projectMetadata, + workspaceRoot, + projectName, + context.builder.builderName, + ); const { runner, diff --git a/packages/angular/build/src/utils/normalize-cache.ts b/packages/angular/build/src/utils/normalize-cache.ts index 6eccc562aadb..a1a03fb126ac 100644 --- a/packages/angular/build/src/utils/normalize-cache.ts +++ b/packages/angular/build/src/utils/normalize-cache.ts @@ -97,9 +97,24 @@ function getCacheBasePath(workspaceRoot: string, cachePathSetting: string): stri return resolve(workspaceRoot, cachePathSetting); } +/** + * Normalizes the persistent disk cache configuration for a project or workspace. + * + * Resolves whether disk caching is enabled based on the CLI cache metadata, current runtime + * environment (local vs. CI, or WebContainers), and computes the versioned cache directory paths + * (resolving shared Git worktree paths when applicable). + * + * @param projectMetadata The project or workspace metadata object containing optional `cli.cache` settings. + * @param workspaceRoot The absolute path to the workspace root directory. + * @param projectName Optional name of the project used to scope the resolved cache path. + * @param builderName Optional name of the builder or tool used to further scope the resolved cache path. + * @returns The normalized disk cache options including enabled state and resolved directory paths. + */ export function normalizeCacheOptions( projectMetadata: unknown, workspaceRoot: string, + projectName = '', + builderName = '', ): NormalizedCachedOptions { const cacheMetadata = hasCacheMetadata(projectMetadata) ? projectMetadata.cli.cache : {}; @@ -129,8 +144,8 @@ export function normalizeCacheOptions( return { enabled: cacheEnabled, basePath: cacheBasePath, - path: join(cacheBasePath, VERSION), + path: join(cacheBasePath, VERSION, projectName, builderName), localBasePath: localCacheBasePath, - localPath: join(localCacheBasePath, VERSION), + localPath: join(localCacheBasePath, VERSION, projectName, builderName), }; } diff --git a/packages/angular/build/src/utils/normalize-cache_spec.ts b/packages/angular/build/src/utils/normalize-cache_spec.ts index 3d423f21e2e6..b8862a0f9ac6 100644 --- a/packages/angular/build/src/utils/normalize-cache_spec.ts +++ b/packages/angular/build/src/utils/normalize-cache_spec.ts @@ -197,4 +197,82 @@ describe('normalizeCacheOptions', () => { expect(options.localBasePath).toBe(resolve(workspaceRoot, '.angular/cache')); expect(options.localPath).toBe(resolve(workspaceRoot, '.angular/cache', '0.0.0-PLACEHOLDER')); }); + + it('should scope path and localPath to projectName when provided', async () => { + const workspaceRoot = join(tempDir, 'project'); + await mkdir(join(workspaceRoot, '.git'), { recursive: true }); + + const options = normalizeCacheOptions({}, workspaceRoot, 'my-app'); + + expect(options.basePath).toBe(resolve(workspaceRoot, '.angular/cache')); + expect(options.path).toBe(resolve(workspaceRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-app')); + expect(options.localBasePath).toBe(resolve(workspaceRoot, '.angular/cache')); + expect(options.localPath).toBe( + resolve(workspaceRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-app'), + ); + }); + + it('should scope path and localPath to projectName in a git worktree', 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({}, worktreeRoot, 'my-lib'); + + expect(options.basePath).toBe(resolve(mainRepoRoot, '.angular/cache')); + expect(options.path).toBe(resolve(mainRepoRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-lib')); + expect(options.localBasePath).toBe(resolve(worktreeRoot, '.angular/cache')); + expect(options.localPath).toBe( + resolve(worktreeRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-lib'), + ); + }); + + it('should scope path and localPath to projectName and builderName when provided', async () => { + const workspaceRoot = join(tempDir, 'project'); + await mkdir(join(workspaceRoot, '.git'), { recursive: true }); + + const options = normalizeCacheOptions({}, workspaceRoot, 'my-app', 'vite'); + + expect(options.basePath).toBe(resolve(workspaceRoot, '.angular/cache')); + expect(options.path).toBe( + resolve(workspaceRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-app/vite'), + ); + expect(options.localBasePath).toBe(resolve(workspaceRoot, '.angular/cache')); + expect(options.localPath).toBe( + resolve(workspaceRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-app/vite'), + ); + }); + + it('should scope path and localPath to projectName and builderName in a git worktree', 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({}, worktreeRoot, 'my-app', 'vite'); + + expect(options.basePath).toBe(resolve(mainRepoRoot, '.angular/cache')); + expect(options.path).toBe( + resolve(mainRepoRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-app/vite'), + ); + expect(options.localBasePath).toBe(resolve(worktreeRoot, '.angular/cache')); + expect(options.localPath).toBe( + resolve(worktreeRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-app/vite'), + ); + }); }); From 147fe4d3450b0603e8b84ba3ff3f4dde6130da8a Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:02:47 +0000 Subject: [PATCH 2/2] fixup! fix(@angular/build): scope cache directory by project and builder name --- .../build/src/builders/dev-server/vite/server.ts | 5 ++++- .../build/src/builders/ng-packagr/builder.ts | 2 +- .../build/src/builders/unit-test/builder.ts | 2 +- .../angular/build/src/utils/normalize-cache.ts | 5 +++-- .../build/src/utils/normalize-cache_spec.ts | 14 ++++++++++++++ 5 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/angular/build/src/builders/dev-server/vite/server.ts b/packages/angular/build/src/builders/dev-server/vite/server.ts index 1564099480b4..52b83a2b5f1a 100644 --- a/packages/angular/build/src/builders/dev-server/vite/server.ts +++ b/packages/angular/build/src/builders/dev-server/vite/server.ts @@ -165,7 +165,10 @@ export async function setupServer( */ const preTransformRequests = externalMetadata.explicitBrowser.length === 0 && ssrMode === ServerSsrMode.NoSsr; - const cacheDir = serverOptions.cacheOptions.localPath ?? serverOptions.cacheOptions.path; + const cacheDir = join( + serverOptions.cacheOptions.localPath ?? serverOptions.cacheOptions.path, + 'vite', + ); const configuration: Vite.InlineConfig = { configFile: false, diff --git a/packages/angular/build/src/builders/ng-packagr/builder.ts b/packages/angular/build/src/builders/ng-packagr/builder.ts index ea416ff5edc8..9e16e5fe54fb 100644 --- a/packages/angular/build/src/builders/ng-packagr/builder.ts +++ b/packages/angular/build/src/builders/ng-packagr/builder.ts @@ -73,7 +73,7 @@ export async function* execute( const ngPackagrOptions: NgPackagrOptions = { cacheEnabled, poll: options.poll, - cacheDirectory: join(cacheDirectory, 'ng-packagr'), + cacheDirectory: cacheDirectory, }; try { diff --git a/packages/angular/build/src/builders/unit-test/builder.ts b/packages/angular/build/src/builders/unit-test/builder.ts index b496bd4c5d63..bfb4cae0dcdf 100644 --- a/packages/angular/build/src/builders/unit-test/builder.ts +++ b/packages/angular/build/src/builders/unit-test/builder.ts @@ -340,7 +340,7 @@ export async function* execute( } satisfies ApplicationBuilderInternalOptions; const dumpDirectory = normalizedOptions.dumpVirtualFiles - ? path.join(normalizedOptions.cacheOptions.path, 'unit-test', 'output-files') + ? path.join(normalizedOptions.cacheOptions.path, 'output-files') : undefined; yield* runBuildAndTest( diff --git a/packages/angular/build/src/utils/normalize-cache.ts b/packages/angular/build/src/utils/normalize-cache.ts index a1a03fb126ac..e24eaf8627c8 100644 --- a/packages/angular/build/src/utils/normalize-cache.ts +++ b/packages/angular/build/src/utils/normalize-cache.ts @@ -140,12 +140,13 @@ export function normalizeCacheOptions( const cacheBasePath = getCacheBasePath(workspaceRoot, path); const localCacheBasePath = isAbsolute(path) ? path : resolve(workspaceRoot, path); + const normalizedBuilderName = builderName.replaceAll(':', '-'); return { enabled: cacheEnabled, basePath: cacheBasePath, - path: join(cacheBasePath, VERSION, projectName, builderName), + path: join(cacheBasePath, VERSION, projectName, normalizedBuilderName), localBasePath: localCacheBasePath, - localPath: join(localCacheBasePath, VERSION, projectName, builderName), + localPath: join(localCacheBasePath, VERSION, projectName, normalizedBuilderName), }; } diff --git a/packages/angular/build/src/utils/normalize-cache_spec.ts b/packages/angular/build/src/utils/normalize-cache_spec.ts index b8862a0f9ac6..18e75403756d 100644 --- a/packages/angular/build/src/utils/normalize-cache_spec.ts +++ b/packages/angular/build/src/utils/normalize-cache_spec.ts @@ -275,4 +275,18 @@ describe('normalizeCacheOptions', () => { resolve(worktreeRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-app/vite'), ); }); + + it('should normalize builderName to avoid invalid path characters', async () => { + const workspaceRoot = join(tempDir, 'project'); + await mkdir(join(workspaceRoot, '.git'), { recursive: true }); + + const options = normalizeCacheOptions({}, workspaceRoot, 'my-app', '@angular/build:unit-test'); + + expect(options.path).toBe( + resolve(workspaceRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-app/@angular/build-unit-test'), + ); + expect(options.localPath).toBe( + resolve(workspaceRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-app/@angular/build-unit-test'), + ); + }); });