Skip to content

Commit 5ffed3f

Browse files
authored
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.
1 parent 4244eb7 commit 5ffed3f

9 files changed

Lines changed: 138 additions & 11 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: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,6 @@ export async function setupServer(
167167
externalMetadata.explicitBrowser.length === 0 && ssrMode === ServerSsrMode.NoSsr;
168168
const cacheDir = join(
169169
serverOptions.cacheOptions.localPath ?? serverOptions.cacheOptions.path,
170-
serverOptions.buildTarget.project,
171170
'vite',
172171
);
173172

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,12 @@ export async function normalizeLibraryOptions(
112112
projectSourceRoot,
113113
);
114114

115-
const cacheOptions = normalizeCacheOptions(projectMetadata, workspaceRoot);
115+
const cacheOptions = normalizeCacheOptions(
116+
projectMetadata,
117+
workspaceRoot,
118+
projectName,
119+
context.builder.builderName,
120+
);
116121

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

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,14 @@ 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 = {
7274
cacheEnabled,
7375
poll: options.poll,
74-
cacheDirectory: join(cacheDirectory, 'ng-packagr'),
76+
cacheDirectory: cacheDirectory,
7577
};
7678

7779
try {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ export async function* execute(
340340
} satisfies ApplicationBuilderInternalOptions;
341341

342342
const dumpDirectory = normalizedOptions.dumpVirtualFiles
343-
? path.join(normalizedOptions.cacheOptions.path, 'unit-test', 'output-files')
343+
? path.join(normalizedOptions.cacheOptions.path, 'output-files')
344344
: undefined;
345345

346346
yield* runBuildAndTest(

‎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: 18 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 = '',
117+
builderName = '',
103118
): NormalizedCachedOptions {
104119
const cacheMetadata = hasCacheMetadata(projectMetadata) ? projectMetadata.cli.cache : {};
105120

@@ -125,12 +140,13 @@ export function normalizeCacheOptions(
125140

126141
const cacheBasePath = getCacheBasePath(workspaceRoot, path);
127142
const localCacheBasePath = isAbsolute(path) ? path : resolve(workspaceRoot, path);
143+
const normalizedBuilderName = builderName.replaceAll(':', '-');
128144

129145
return {
130146
enabled: cacheEnabled,
131147
basePath: cacheBasePath,
132-
path: join(cacheBasePath, VERSION),
148+
path: join(cacheBasePath, VERSION, projectName, normalizedBuilderName),
133149
localBasePath: localCacheBasePath,
134-
localPath: join(localCacheBasePath, VERSION),
150+
localPath: join(localCacheBasePath, VERSION, projectName, normalizedBuilderName),
135151
};
136152
}

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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,96 @@ 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+
});
278+
279+
it('should normalize builderName to avoid invalid path characters', async () => {
280+
const workspaceRoot = join(tempDir, 'project');
281+
await mkdir(join(workspaceRoot, '.git'), { recursive: true });
282+
283+
const options = normalizeCacheOptions({}, workspaceRoot, 'my-app', '@angular/build:unit-test');
284+
285+
expect(options.path).toBe(
286+
resolve(workspaceRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-app/@angular/build-unit-test'),
287+
);
288+
expect(options.localPath).toBe(
289+
resolve(workspaceRoot, '.angular/cache/0.0.0-PLACEHOLDER/my-app/@angular/build-unit-test'),
290+
);
291+
});
200292
});

0 commit comments

Comments
 (0)