Skip to content

Commit ead7184

Browse files
committed
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.
1 parent 88ebc67 commit ead7184

8 files changed

Lines changed: 122 additions & 13 deletions

File tree

‎packages/angular/build/src/builders/application/options.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,12 @@ export async function normalizeOptions(
182182
const { projectRoot, projectSourceRoot } = getProjectRootPaths(workspaceRoot, projectMetadata);
183183

184184
// Gather persistent caching option and provide a project specific cache location
185-
const cacheOptions = normalizeCacheOptions(projectMetadata, workspaceRoot);
186-
cacheOptions.path = path.join(cacheOptions.path, projectName);
185+
const cacheOptions = normalizeCacheOptions(
186+
projectMetadata,
187+
workspaceRoot,
188+
projectName,
189+
context.builder.builderName,
190+
);
187191

188192
const i18nOptions: I18nOptions & {
189193
duplicateTranslationBehavior?: I18NTranslation;

‎packages/angular/build/src/builders/dev-server/options.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ export async function normalizeOptions(
3939
const projectMetadata = await context.getProjectMetadata(projectName);
4040
const projectRoot = path.join(workspaceRoot, (projectMetadata.root as string | undefined) ?? '');
4141

42-
const cacheOptions = normalizeCacheOptions(projectMetadata, workspaceRoot);
42+
const cacheOptions = normalizeCacheOptions(
43+
projectMetadata,
44+
workspaceRoot,
45+
projectName,
46+
context.builder.builderName,
47+
);
4348

4449
// Target specifier defaults to the current project's build target using a development configuration
4550
const buildTargetSpecifier = options.buildTarget ?? `::development`;

‎packages/angular/build/src/builders/dev-server/vite/server.ts‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,7 @@ export async function setupServer(
165165
*/
166166
const preTransformRequests =
167167
externalMetadata.explicitBrowser.length === 0 && ssrMode === ServerSsrMode.NoSsr;
168-
const cacheDir = join(
169-
serverOptions.cacheOptions.localPath ?? serverOptions.cacheOptions.path,
170-
serverOptions.buildTarget.project,
171-
'vite',
172-
);
168+
const cacheDir = serverOptions.cacheOptions.localPath ?? serverOptions.cacheOptions.path;
173169

174170
const configuration: Vite.InlineConfig = {
175171
configFile: false,

‎packages/angular/build/src/builders/library/options.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,12 @@ export async function normalizeLibraryOptions(
126126
projectSourceRoot,
127127
);
128128

129-
const cacheOptions = normalizeCacheOptions(projectMetadata, workspaceRoot);
129+
const cacheOptions = normalizeCacheOptions(
130+
projectMetadata,
131+
workspaceRoot,
132+
projectName,
133+
context.builder.builderName,
134+
);
130135

131136
const styleIncludePaths = (stylePreprocessorOptions?.includePaths ?? []).map((p: string) =>
132137
path.resolve(workspaceRoot, p),

‎packages/angular/build/src/builders/ng-packagr/builder.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export async function* execute(
6666
const { enabled: cacheEnabled, path: cacheDirectory } = normalizeCacheOptions(
6767
metadata,
6868
context.workspaceRoot,
69+
projectName,
70+
context.builder.builderName,
6971
);
7072

7173
const ngPackagrOptions: NgPackagrOptions = {

‎packages/angular/build/src/builders/unit-test/options.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,12 @@ export async function normalizeOptions(
6464
const { projectRoot, projectSourceRoot } = getProjectRootPaths(workspaceRoot, projectMetadata);
6565

6666
// Gather persistent caching option and provide a project specific cache location
67-
const cacheOptions = normalizeCacheOptions(projectMetadata, workspaceRoot);
68-
cacheOptions.path = path.join(cacheOptions.path, projectName);
67+
const cacheOptions = normalizeCacheOptions(
68+
projectMetadata,
69+
workspaceRoot,
70+
projectName,
71+
context.builder.builderName,
72+
);
6973

7074
const {
7175
runner,

‎packages/angular/build/src/utils/normalize-cache.ts‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,24 @@ function getCacheBasePath(workspaceRoot: string, cachePathSetting: string): stri
9797
return resolve(workspaceRoot, cachePathSetting);
9898
}
9999

100+
/**
101+
* Normalizes the persistent disk cache configuration for a project or workspace.
102+
*
103+
* Resolves whether disk caching is enabled based on the CLI cache metadata, current runtime
104+
* environment (local vs. CI, or WebContainers), and computes the versioned cache directory paths
105+
* (resolving shared Git worktree paths when applicable).
106+
*
107+
* @param projectMetadata The project or workspace metadata object containing optional `cli.cache` settings.
108+
* @param workspaceRoot The absolute path to the workspace root directory.
109+
* @param projectName Optional name of the project used to scope the resolved cache path.
110+
* @param builderName Optional name of the builder or tool used to further scope the resolved cache path.
111+
* @returns The normalized disk cache options including enabled state and resolved directory paths.
112+
*/
100113
export function normalizeCacheOptions(
101114
projectMetadata: unknown,
102115
workspaceRoot: string,
116+
projectName: string = '',
117+
builderName: string = '',
103118
): NormalizedCachedOptions {
104119
const cacheMetadata = hasCacheMetadata(projectMetadata) ? projectMetadata.cli.cache : {};
105120

@@ -129,8 +144,8 @@ export function normalizeCacheOptions(
129144
return {
130145
enabled: cacheEnabled,
131146
basePath: cacheBasePath,
132-
path: join(cacheBasePath, VERSION),
147+
path: join(cacheBasePath, VERSION, projectName, builderName),
133148
localBasePath: localCacheBasePath,
134-
localPath: join(localCacheBasePath, VERSION),
149+
localPath: join(localCacheBasePath, VERSION, projectName, builderName),
135150
};
136151
}

‎packages/angular/build/src/utils/normalize-cache_spec.ts‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,82 @@ describe('normalizeCacheOptions', () => {
197197
expect(options.localBasePath).toBe(resolve(workspaceRoot, '.angular/cache'));
198198
expect(options.localPath).toBe(resolve(workspaceRoot, '.angular/cache', '0.0.0-PLACEHOLDER'));
199199
});
200+
201+
it('should scope path and localPath to projectName when provided', async () => {
202+
const workspaceRoot = join(tempDir, 'project');
203+
await mkdir(join(workspaceRoot, '.git'), { recursive: true });
204+
205+
const options = normalizeCacheOptions({}, workspaceRoot, 'my-app');
206+
207+
expect(options.basePath).toBe(resolve(workspaceRoot, '.angular/cache'));
208+
expect(options.path).toBe(resolve(workspaceRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-app'));
209+
expect(options.localBasePath).toBe(resolve(workspaceRoot, '.angular/cache'));
210+
expect(options.localPath).toBe(
211+
resolve(workspaceRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-app'),
212+
);
213+
});
214+
215+
it('should scope path and localPath to projectName in a git worktree', async () => {
216+
const mainRepoRoot = join(tempDir, 'main-repo');
217+
const mainGitDir = join(mainRepoRoot, '.git');
218+
const worktreeRoot = join(tempDir, 'worktree');
219+
220+
await mkdir(mainGitDir, { recursive: true });
221+
222+
const worktreeMetadataDir = join(mainGitDir, 'worktrees/wt-1');
223+
await mkdir(worktreeMetadataDir, { recursive: true });
224+
await mkdir(worktreeRoot, { recursive: true });
225+
await writeFile(join(worktreeRoot, '.git'), `gitdir: ${worktreeMetadataDir}`);
226+
await writeFile(join(worktreeMetadataDir, 'commondir'), '../..');
227+
228+
const options = normalizeCacheOptions({}, worktreeRoot, 'my-lib');
229+
230+
expect(options.basePath).toBe(resolve(mainRepoRoot, '.angular/cache'));
231+
expect(options.path).toBe(resolve(mainRepoRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-lib'));
232+
expect(options.localBasePath).toBe(resolve(worktreeRoot, '.angular/cache'));
233+
expect(options.localPath).toBe(
234+
resolve(worktreeRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-lib'),
235+
);
236+
});
237+
238+
it('should scope path and localPath to projectName and builderName when provided', async () => {
239+
const workspaceRoot = join(tempDir, 'project');
240+
await mkdir(join(workspaceRoot, '.git'), { recursive: true });
241+
242+
const options = normalizeCacheOptions({}, workspaceRoot, 'my-app', 'vite');
243+
244+
expect(options.basePath).toBe(resolve(workspaceRoot, '.angular/cache'));
245+
expect(options.path).toBe(
246+
resolve(workspaceRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-app/vite'),
247+
);
248+
expect(options.localBasePath).toBe(resolve(workspaceRoot, '.angular/cache'));
249+
expect(options.localPath).toBe(
250+
resolve(workspaceRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-app/vite'),
251+
);
252+
});
253+
254+
it('should scope path and localPath to projectName and builderName in a git worktree', async () => {
255+
const mainRepoRoot = join(tempDir, 'main-repo');
256+
const mainGitDir = join(mainRepoRoot, '.git');
257+
const worktreeRoot = join(tempDir, 'worktree');
258+
259+
await mkdir(mainGitDir, { recursive: true });
260+
261+
const worktreeMetadataDir = join(mainGitDir, 'worktrees/wt-1');
262+
await mkdir(worktreeMetadataDir, { recursive: true });
263+
await mkdir(worktreeRoot, { recursive: true });
264+
await writeFile(join(worktreeRoot, '.git'), `gitdir: ${worktreeMetadataDir}`);
265+
await writeFile(join(worktreeMetadataDir, 'commondir'), '../..');
266+
267+
const options = normalizeCacheOptions({}, worktreeRoot, 'my-app', 'vite');
268+
269+
expect(options.basePath).toBe(resolve(mainRepoRoot, '.angular/cache'));
270+
expect(options.path).toBe(
271+
resolve(mainRepoRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-app/vite'),
272+
);
273+
expect(options.localBasePath).toBe(resolve(worktreeRoot, '.angular/cache'));
274+
expect(options.localPath).toBe(
275+
resolve(worktreeRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-app/vite'),
276+
);
277+
});
200278
});

0 commit comments

Comments
 (0)