Skip to content

Commit 8786daa

Browse files
committed
refactor(@angular-devkit/build-angular): create reusable internal cache infrastructure
Several parts of application builder use slightly different variants of an in-memory cache. To avoid duplication of code, unified cache infrastructure is now available for use internally. This also allows for expanded use in other areas of the build system including the future potential for adding additional backing cache stores.
1 parent af43c34 commit 8786daa

File tree

3 files changed

+137
-32
lines changed

3 files changed

+137
-32
lines changed

packages/angular_devkit/build_angular/src/tools/esbuild/angular/component-stylesheets.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,20 @@ import { OutputFile } from 'esbuild';
1010
import { createHash } from 'node:crypto';
1111
import path from 'node:path';
1212
import { BuildOutputFileType, BundleContextResult, BundlerContext } from '../bundler-context';
13+
import { MemoryCache } from '../cache';
1314
import {
1415
BundleStylesheetOptions,
1516
createStylesheetBundleOptions,
1617
} from '../stylesheets/bundle-options';
1718

18-
class BundlerContextCache extends Map<string, BundlerContext> {
19-
getOrCreate(key: string, creator: () => BundlerContext): BundlerContext {
20-
let value = this.get(key);
21-
22-
if (value === undefined) {
23-
value = creator();
24-
this.set(key, value);
25-
}
26-
27-
return value;
28-
}
29-
}
30-
3119
/**
3220
* Bundles component stylesheets. A stylesheet can be either an inline stylesheet that
3321
* is contained within the Component's metadata definition or an external file referenced
3422
* from the Component's metadata definition.
3523
*/
3624
export class ComponentStylesheetBundler {
37-
readonly #fileContexts = new BundlerContextCache();
38-
readonly #inlineContexts = new BundlerContextCache();
25+
readonly #fileContexts = new MemoryCache<BundlerContext>();
26+
readonly #inlineContexts = new MemoryCache<BundlerContext>();
3927

4028
/**
4129
*
@@ -48,7 +36,7 @@ export class ComponentStylesheetBundler {
4836
) {}
4937

5038
async bundleFile(entry: string) {
51-
const bundlerContext = this.#fileContexts.getOrCreate(entry, () => {
39+
const bundlerContext = await this.#fileContexts.getOrCreate(entry, () => {
5240
return new BundlerContext(this.options.workspaceRoot, this.incremental, (loadCache) => {
5341
const buildOptions = createStylesheetBundleOptions(this.options, loadCache);
5442
buildOptions.entryPoints = [entry];
@@ -67,7 +55,7 @@ export class ComponentStylesheetBundler {
6755
const id = createHash('sha256').update(data).digest('hex');
6856
const entry = [language, id, filename].join(';');
6957

70-
const bundlerContext = this.#inlineContexts.getOrCreate(entry, () => {
58+
const bundlerContext = await this.#inlineContexts.getOrCreate(entry, () => {
7159
const namespace = 'angular:styles/component';
7260

7361
return new BundlerContext(this.options.workspaceRoot, this.incremental, (loadCache) => {
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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+
/**
10+
* @fileoverview
11+
* Provides infrastructure for common caching functionality within the build system.
12+
*/
13+
14+
/**
15+
* A backing data store for one or more Cache instances.
16+
* The interface is intentionally designed to support using a JavaScript
17+
* Map instance as a potential cache store.
18+
*/
19+
export interface CacheStore<V> {
20+
/**
21+
* Returns the specified value from the cache store or `undefined` if not found.
22+
* @param key The key to retrieve from the store.
23+
*/
24+
get(key: string): V | undefined | Promise<V | undefined>;
25+
26+
/**
27+
* Returns whether the provided key is present in the cache store.
28+
* @param key The key to check from the store.
29+
*/
30+
has(key: string): boolean | Promise<boolean>;
31+
32+
/**
33+
* Adds a new value to the cache store if the key is not present.
34+
* Updates the value for the key if already present.
35+
* @param key The key to associate with the value in the cache store.
36+
* @param value The value to add to the cache store.
37+
*/
38+
set(key: string, value: V): this | Promise<this>;
39+
}
40+
41+
/**
42+
* A cache object that allows accessing and storing key/value pairs in
43+
* an underlying CacheStore. This class is the primary method for consumers
44+
* to use a cache.
45+
*/
46+
export class Cache<V, S extends CacheStore<V> = CacheStore<V>> {
47+
constructor(
48+
protected readonly store: S,
49+
readonly namespace?: string,
50+
) {}
51+
52+
/**
53+
* Prefixes a key with the cache namespace if present.
54+
* @param key A key string to prefix.
55+
* @returns A prefixed key if a namespace is present. Otherwise the provided key.
56+
*/
57+
protected withNamespace(key: string): string {
58+
if (this.namespace) {
59+
return `${this.namespace}:${key}`;
60+
}
61+
62+
return key;
63+
}
64+
65+
/**
66+
* Gets the value associated with a provided key if available.
67+
* Otherwise, creates a value using the factory creator function, puts the value
68+
* in the cache, and returns the new value.
69+
* @param key A key associated with the value.
70+
* @param creator A factory function for the value if no value is present.
71+
* @returns A value associated with the provided key.
72+
*/
73+
async getOrCreate(key: string, creator: () => V | Promise<V>): Promise<V> {
74+
const namespacedKey = this.withNamespace(key);
75+
let value = await this.store.get(namespacedKey);
76+
77+
if (value === undefined) {
78+
value = await creator();
79+
await this.store.set(namespacedKey, value);
80+
}
81+
82+
return value;
83+
}
84+
85+
/**
86+
* Gets the value associated with a provided key if available.
87+
* @param key A key associated with the value.
88+
* @returns A value associated with the provided key if present. Otherwise, `undefined`.
89+
*/
90+
async get(key: string): Promise<V | undefined> {
91+
const value = await this.store.get(this.withNamespace(key));
92+
93+
return value;
94+
}
95+
96+
/**
97+
* Puts a value in the cache and associates it with the provided key.
98+
* If the key is already present, the value is updated instead.
99+
* @param key A key associated with the value.
100+
* @param value A value to put in the cache.
101+
*/
102+
async put(key: string, value: V): Promise<void> {
103+
await this.store.set(this.withNamespace(key), value);
104+
}
105+
}
106+
107+
/**
108+
* A lightweight in-memory cache implementation based on a JavaScript Map object.
109+
*/
110+
export class MemoryCache<V> extends Cache<V, Map<string, V>> {
111+
constructor() {
112+
super(new Map());
113+
}
114+
115+
/**
116+
* Removes all entries from the cache instance.
117+
*/
118+
clear() {
119+
this.store.clear();
120+
}
121+
122+
/**
123+
* Provides all the values currently present in the cache instance.
124+
* @returns An iterable of all values in the cache.
125+
*/
126+
values() {
127+
return this.store.values();
128+
}
129+
}

packages/angular_devkit/build_angular/src/tools/esbuild/stylesheets/sass-language.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { dirname, join, relative } from 'node:path';
1111
import { fileURLToPath, pathToFileURL } from 'node:url';
1212
import type { CanonicalizeContext, CompileResult, Exception, Syntax } from 'sass';
1313
import type { SassWorkerImplementation } from '../../sass/sass-service';
14+
import { MemoryCache } from '../cache';
1415
import { StylesheetLanguage, StylesheetPluginOptions } from './stylesheet-plugin-factory';
1516

1617
let sassWorkerPool: SassWorkerImplementation | undefined;
@@ -68,19 +69,6 @@ function parsePackageName(url: string): { packageName: string; readonly pathSegm
6869
};
6970
}
7071

71-
class Cache<K, V> extends Map<K, V> {
72-
async getOrCreate(key: K, creator: () => V | Promise<V>): Promise<V> {
73-
let value = this.get(key);
74-
75-
if (value === undefined) {
76-
value = await creator();
77-
this.set(key, value);
78-
}
79-
80-
return value;
81-
}
82-
}
83-
8472
async function compileString(
8573
data: string,
8674
filePath: string,
@@ -104,8 +92,8 @@ async function compileString(
10492
// A null value indicates that the cached resolution attempt failed to find a location and
10593
// later stage resolution should be attempted. This avoids potentially expensive repeat
10694
// failing resolution attempts.
107-
const resolutionCache = new Cache<string, URL | null>();
108-
const packageRootCache = new Cache<string, string | null>();
95+
const resolutionCache = new MemoryCache<URL | null>();
96+
const packageRootCache = new MemoryCache<string | null>();
10997

11098
const warnings: PartialMessage[] = [];
11199
try {

0 commit comments

Comments
 (0)