Skip to content

Commit 6bfd180

Browse files
clydindgp1130
authored andcommittedJul 18, 2023
perf(@angular-devkit/build-angular): use in-memory Sass module resolution cache
When using Sass files with module import references in the esbuild-based browser application builder, the module resolution attempts will now be cached in memory. This caching is only local to each entry Sass file currently. However, this may be expanded to encompass all Sass entries within a build in the future. In addition to caching the entire resolution attempt, individual package root resolution is also cached for deep import attempts. This is useful for packages (such as `@material/*`) which deep import to multiple different files in the same package. With this change combined with the previous lexer change, package managers (pnpm & Yarn PnP) that require workarounds for functioning resolution will now perform at an equivalent level to other package managers.
1 parent c34bbb8 commit 6bfd180

File tree

1 file changed

+66
-30
lines changed
  • packages/angular_devkit/build_angular/src/tools/esbuild/stylesheets

1 file changed

+66
-30
lines changed
 

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

+66-30
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,33 @@ export const SassStylesheetLanguage = Object.freeze<StylesheetLanguage>({
6565
},
6666
});
6767

68+
function parsePackageName(url: string): { packageName: string; readonly pathSegments: string[] } {
69+
const parts = url.split('/');
70+
const hasScope = parts.length >= 2 && parts[0].startsWith('@');
71+
const [nameOrScope, nameOrFirstPath, ...pathPart] = parts;
72+
const packageName = hasScope ? `${nameOrScope}/${nameOrFirstPath}` : nameOrScope;
73+
74+
return {
75+
packageName,
76+
get pathSegments() {
77+
return !hasScope && nameOrFirstPath ? [nameOrFirstPath, ...pathPart] : pathPart;
78+
},
79+
};
80+
}
81+
82+
class Cache<K, V> extends Map<K, V> {
83+
async getOrCreate(key: K, creator: () => V | Promise<V>): Promise<V> {
84+
let value = this.get(key);
85+
86+
if (value === undefined) {
87+
value = await creator();
88+
this.set(key, value);
89+
}
90+
91+
return value;
92+
}
93+
}
94+
6895
async function compileString(
6996
data: string,
7097
filePath: string,
@@ -81,6 +108,15 @@ async function compileString(
81108
sassWorkerPool = new sassService.SassWorkerImplementation(true);
82109
}
83110

111+
// Cache is currently local to individual compile requests.
112+
// Caching follows Sass behavior where a given url will always resolve to the same value
113+
// regardless of its importer's path.
114+
// A null value indicates that the cached resolution attempt failed to find a location and
115+
// later stage resolution should be attempted. This avoids potentially expensive repeat
116+
// failing resolution attempts.
117+
const resolutionCache = new Cache<string, URL | null>();
118+
const packageRootCache = new Cache<string, string | null>();
119+
84120
const warnings: PartialMessage[] = [];
85121
try {
86122
const { css, sourceMap, loadedUrls } = await sassWorkerPool.compileStringAsync(data, {
@@ -93,36 +129,36 @@ async function compileString(
93129
quietDeps: true,
94130
importers: [
95131
{
96-
findFileUrl: async (
97-
url,
98-
options: FileImporterWithRequestContextOptions,
99-
): Promise<URL | null> => {
100-
const result = await resolveUrl(url, options);
101-
if (result.path) {
102-
return pathToFileURL(result.path);
103-
}
104-
105-
// Check for package deep imports
106-
const parts = url.split('/');
107-
const hasScope = parts.length >= 2 && parts[0].startsWith('@');
108-
const [nameOrScope, nameOrFirstPath, ...pathPart] = parts;
109-
const packageName = hasScope ? `${nameOrScope}/${nameOrFirstPath}` : nameOrScope;
110-
111-
const packageResult = await resolveUrl(packageName + '/package.json', options);
112-
113-
if (packageResult.path) {
114-
return pathToFileURL(
115-
join(
116-
dirname(packageResult.path),
117-
!hasScope && nameOrFirstPath ? nameOrFirstPath : '',
118-
...pathPart,
119-
),
120-
);
121-
}
122-
123-
// Not found
124-
return null;
125-
},
132+
findFileUrl: (url, options: FileImporterWithRequestContextOptions) =>
133+
resolutionCache.getOrCreate(url, async () => {
134+
const result = await resolveUrl(url, options);
135+
if (result.path) {
136+
return pathToFileURL(result.path);
137+
}
138+
139+
// Check for package deep imports
140+
const { packageName, pathSegments } = parsePackageName(url);
141+
142+
// Caching package root locations is particularly beneficial for `@material/*` packages
143+
// which extensively use deep imports.
144+
const packageRoot = await packageRootCache.getOrCreate(packageName, async () => {
145+
// Use the required presence of a package root `package.json` file to resolve the location
146+
const packageResult = await resolveUrl(packageName + '/package.json', options);
147+
148+
return packageResult.path ? dirname(packageResult.path) : null;
149+
});
150+
151+
// Package not found could be because of an error or the specifier is intended to be found
152+
// via a later stage of the resolution process (`loadPaths`, etc.).
153+
// Errors are reported after the full completion of the resolution process. Exceptions for
154+
// not found packages should not be raised here.
155+
if (packageRoot) {
156+
return pathToFileURL(join(packageRoot, ...pathSegments));
157+
}
158+
159+
// Not found
160+
return null;
161+
}),
126162
},
127163
],
128164
logger: {

0 commit comments

Comments
 (0)
Please sign in to comment.