Skip to content

Commit 6d2087b

Browse files
alan-agius4dgp1130
authored andcommitted
fix(@angular-devkit/build-angular): automatically purge stale build cache entries
With every build-angular release, previously created cache entries get stale and are no longer used. This causes the cache to keep growing as older files are not purged. With this change we automatically purge entries that have been created with older version of build-angular and can no longer be used with the current installed version. Closes #22323
1 parent cbe028e commit 6d2087b

File tree

13 files changed

+129
-9
lines changed

13 files changed

+129
-9
lines changed

packages/angular_devkit/build_angular/src/babel/webpack-loader.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import remapping from '@ampproject/remapping';
1010
import { custom } from 'babel-loader';
1111
import { ScriptTarget } from 'typescript';
1212
import { loadEsmModule } from '../utils/load-esm';
13+
import { VERSION } from '../utils/package-version';
1314
import { ApplicationPresetOptions, I18nPluginCreators } from './presets/application';
1415

1516
interface AngularCustomOptions extends Omit<ApplicationPresetOptions, 'instrumentCode'> {
@@ -196,7 +197,7 @@ export default custom<ApplicationPresetOptions>(() => {
196197
...baseOptions,
197198
...rawOptions,
198199
cacheIdentifier: JSON.stringify({
199-
buildAngular: require('../../package.json').version,
200+
buildAngular: VERSION,
200201
customOptions,
201202
baseOptions,
202203
rawOptions,

packages/angular_devkit/build_angular/src/builders/browser/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
import { normalizeCacheOptions } from '../../utils/normalize-cache';
4040
import { ensureOutputPaths } from '../../utils/output-paths';
4141
import { generateEntryPoints } from '../../utils/package-chunk-sort';
42+
import { purgeStaleBuildCache } from '../../utils/purge-cache';
4243
import { augmentAppWithServiceWorker } from '../../utils/service-worker';
4344
import { Spinner } from '../../utils/spinner';
4445
import { getSupportedBrowsers } from '../../utils/supported-browsers';
@@ -157,6 +158,9 @@ export function buildWebpackBrowser(
157158
),
158159
);
159160

161+
// Purge old build disk cache.
162+
await purgeStaleBuildCache(context);
163+
160164
checkInternetExplorerSupport(sysProjectRoot, context.logger);
161165

162166
return {

packages/angular_devkit/build_angular/src/builders/dev-server/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { IndexHtmlTransform } from '../../utils/index-file/index-html-generator'
2828
import { createTranslationLoader } from '../../utils/load-translations';
2929
import { NormalizedCachedOptions, normalizeCacheOptions } from '../../utils/normalize-cache';
3030
import { generateEntryPoints } from '../../utils/package-chunk-sort';
31+
import { purgeStaleBuildCache } from '../../utils/purge-cache';
3132
import { assertCompatibleAngularVersion } from '../../utils/version';
3233
import {
3334
generateI18nBrowserWebpackConfigFromContext,
@@ -90,6 +91,9 @@ export function serveWebpackBrowser(
9091
throw new Error('The builder requires a target.');
9192
}
9293

94+
// Purge old build disk cache.
95+
await purgeStaleBuildCache(context);
96+
9397
options.port = await checkPort(options.port ?? 4200, options.host || 'localhost');
9498

9599
if (options.hmr) {

packages/angular_devkit/build_angular/src/builders/extract-i18n/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import webpack, { Configuration } from 'webpack';
1717
import { ExecutionTransformer } from '../../transforms';
1818
import { createI18nOptions } from '../../utils/i18n-options';
1919
import { loadEsmModule } from '../../utils/load-esm';
20+
import { purgeStaleBuildCache } from '../../utils/purge-cache';
2021
import { assertCompatibleAngularVersion } from '../../utils/version';
2122
import { generateBrowserWebpackConfigFromContext } from '../../utils/webpack-browser-config';
2223
import { getCommonConfig } from '../../webpack/configs';
@@ -130,6 +131,9 @@ export async function execute(
130131
// Check Angular version.
131132
assertCompatibleAngularVersion(context.workspaceRoot);
132133

134+
// Purge old build disk cache.
135+
await purgeStaleBuildCache(context);
136+
133137
const browserTarget = targetFromTargetString(options.browserTarget);
134138
const browserOptions = await context.validateOptions<JsonObject & BrowserBuilderOptions>(
135139
await context.getTargetOptions(browserTarget),

packages/angular_devkit/build_angular/src/builders/karma/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Observable, from } from 'rxjs';
1414
import { defaultIfEmpty, switchMap } from 'rxjs/operators';
1515
import { Configuration } from 'webpack';
1616
import { ExecutionTransformer } from '../../transforms';
17+
import { purgeStaleBuildCache } from '../../utils/purge-cache';
1718
import { assertCompatibleAngularVersion } from '../../utils/version';
1819
import { generateBrowserWebpackConfigFromContext } from '../../utils/webpack-browser-config';
1920
import { getCommonConfig, getStylesConfig } from '../../webpack/configs';
@@ -32,6 +33,9 @@ async function initialize(
3233
context: BuilderContext,
3334
webpackConfigurationTransformer?: ExecutionTransformer<Configuration>,
3435
): Promise<[typeof import('karma'), Configuration]> {
36+
// Purge old build disk cache.
37+
await purgeStaleBuildCache(context);
38+
3539
const { config } = await generateBrowserWebpackConfigFromContext(
3640
// only two properties are missing:
3741
// * `outputPath` which is fixed for tests

packages/angular_devkit/build_angular/src/builders/ng-packagr/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { join, resolve } from 'path';
1111
import { Observable, from, of } from 'rxjs';
1212
import { catchError, mapTo, switchMap } from 'rxjs/operators';
1313
import { normalizeCacheOptions } from '../../utils/normalize-cache';
14+
import { purgeStaleBuildCache } from '../../utils/purge-cache';
1415
import { Schema as NgPackagrBuilderOptions } from './schema';
1516

1617
/**
@@ -22,6 +23,9 @@ export function execute(
2223
): Observable<BuilderOutput> {
2324
return from(
2425
(async () => {
26+
// Purge old build disk cache.
27+
await purgeStaleBuildCache(context);
28+
2529
const root = context.workspaceRoot;
2630
const packager = (await import('ng-packagr')).ngPackagr();
2731

packages/angular_devkit/build_angular/src/builders/server/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { NormalizedBrowserBuilderSchema, deleteOutputDir } from '../../utils';
1919
import { i18nInlineEmittedFiles } from '../../utils/i18n-inlining';
2020
import { I18nOptions } from '../../utils/i18n-options';
2121
import { ensureOutputPaths } from '../../utils/output-paths';
22+
import { purgeStaleBuildCache } from '../../utils/purge-cache';
2223
import { assertCompatibleAngularVersion } from '../../utils/version';
2324
import { generateI18nBrowserWebpackConfigFromContext } from '../../utils/webpack-browser-config';
2425
import { getCommonConfig, getStylesConfig } from '../../webpack/configs';
@@ -146,6 +147,9 @@ async function initialize(
146147
i18n: I18nOptions;
147148
target: ScriptTarget;
148149
}> {
150+
// Purge old build disk cache.
151+
await purgeStaleBuildCache(context);
152+
149153
const originalOutputPath = options.outputPath;
150154
const { config, i18n, target } = await generateI18nBrowserWebpackConfigFromContext(
151155
{

packages/angular_devkit/build_angular/src/utils/index-file/inline-fonts.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@ import proxyAgent from 'https-proxy-agent';
1313
import { join } from 'path';
1414
import { URL } from 'url';
1515
import { NormalizedCachedOptions } from '../normalize-cache';
16+
import { VERSION } from '../package-version';
1617
import { htmlRewritingStream } from './html-rewriting-stream';
1718

18-
const packageVersion = require('../../../package.json').version;
19-
2019
interface FontProviderDetails {
2120
preconnectUrl: string;
2221
}
@@ -156,7 +155,7 @@ export class InlineFontsProcessor {
156155
}
157156

158157
private async getResponse(url: URL): Promise<string> {
159-
const key = `${packageVersion}|${url}`;
158+
const key = `${VERSION}|${url}`;
160159

161160
if (this.cachePath) {
162161
const entry = await cacache.get.info(this.cachePath, key);

packages/angular_devkit/build_angular/src/utils/normalize-cache.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@
77
*/
88

99
import { json } from '@angular-devkit/core';
10-
import { resolve } from 'path';
10+
import { join, resolve } from 'path';
1111
import { cachingDisabled } from './environment-options';
12+
import { VERSION } from './package-version';
1213

1314
export interface NormalizedCachedOptions {
15+
/** Whether disk cache is enabled. */
1416
enabled: boolean;
17+
/** Disk cache path. Example: `/.angular/cache/v12.0.0`. */
1518
path: string;
19+
/** Disk cache base path. Example: `/.angular/cache`. */
20+
basePath: string;
1621
}
1722

1823
interface CacheMetadata {
@@ -49,8 +54,11 @@ export function normalizeCacheOptions(
4954
}
5055
}
5156

57+
const cacheBasePath = resolve(worspaceRoot, path);
58+
5259
return {
5360
enabled: cacheEnabled,
54-
path: resolve(worspaceRoot, path),
61+
basePath: cacheBasePath,
62+
path: join(cacheBasePath, VERSION),
5563
};
5664
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export const VERSION: string = require('../../package.json').version;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { BuilderContext } from '@angular-devkit/architect';
10+
import { PathLike, existsSync, promises as fsPromises } from 'fs';
11+
import { join } from 'path';
12+
import { normalizeCacheOptions } from './normalize-cache';
13+
14+
/** Delete stale cache directories used by previous versions of build-angular. */
15+
export async function purgeStaleBuildCache(context: BuilderContext): Promise<void> {
16+
const projectName = context.target?.project;
17+
if (!projectName) {
18+
return;
19+
}
20+
21+
const metadata = await context.getProjectMetadata(projectName);
22+
const { basePath, path, enabled } = normalizeCacheOptions(metadata, context.workspaceRoot);
23+
24+
if (!enabled || !existsSync(basePath)) {
25+
return;
26+
}
27+
28+
// The below should be removed and replaced with just `rm` when support for Node.Js 12 is removed.
29+
const { rm, rmdir } = fsPromises as typeof fsPromises & {
30+
rm?: (
31+
path: PathLike,
32+
options?: {
33+
force?: boolean;
34+
maxRetries?: number;
35+
recursive?: boolean;
36+
retryDelay?: number;
37+
},
38+
) => Promise<void>;
39+
};
40+
41+
const entriesToDelete = (await fsPromises.readdir(basePath, { withFileTypes: true }))
42+
.filter((d) => join(basePath, d.name) !== path && d.isDirectory())
43+
.map((d) => {
44+
const subPath = join(basePath, d.name);
45+
try {
46+
return rm
47+
? rm(subPath, { force: true, recursive: true, maxRetries: 3 })
48+
: rmdir(subPath, { recursive: true, maxRetries: 3 });
49+
} catch {}
50+
});
51+
52+
await Promise.all(entriesToDelete);
53+
}

packages/angular_devkit/build_angular/src/webpack/utils/helpers.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
ExtraEntryPointClass,
2020
} from '../../builders/browser/schema';
2121
import { WebpackConfigOptions } from '../../utils/build-options';
22+
import { VERSION } from '../../utils/package-version';
2223

2324
export interface HashFormat {
2425
chunk: string;
@@ -122,8 +123,6 @@ export function getCacheSettings(
122123
): WebpackOptionsNormalized['cache'] {
123124
const { enabled, path: cacheDirectory } = wco.buildOptions.cache;
124125
if (enabled) {
125-
const packageVersion = require('../../../package.json').version;
126-
127126
return {
128127
type: 'filesystem',
129128
profile: wco.buildOptions.verbose,
@@ -134,7 +133,7 @@ export function getCacheSettings(
134133
// None of which are "named".
135134
name: createHash('sha1')
136135
.update(angularVersion)
137-
.update(packageVersion)
136+
.update(VERSION)
138137
.update(wco.projectRoot)
139138
.update(JSON.stringify(wco.tsConfig))
140139
.update(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { join } from 'path';
2+
import { createDir, expectFileNotToExist, expectFileToExist } from '../../utils/fs';
3+
import { ng } from '../../utils/process';
4+
import { updateJsonFile } from '../../utils/project';
5+
6+
export default async function () {
7+
const cachePath = '.angular/cache';
8+
const staleCachePath = join(cachePath, 'v1.0.0');
9+
10+
// Enable cache for all environments
11+
await updateJsonFile('angular.json', (config) => {
12+
config.cli ??= {};
13+
config.cli.cache = {
14+
environment: 'all',
15+
enabled: true,
16+
path: cachePath,
17+
};
18+
});
19+
20+
// Create a dummy stale disk cache directory.
21+
await createDir(staleCachePath);
22+
await expectFileToExist(staleCachePath);
23+
24+
await ng('build');
25+
await expectFileToExist(cachePath);
26+
await expectFileNotToExist(staleCachePath);
27+
}

0 commit comments

Comments
 (0)