Skip to content

Commit bc8578c

Browse files
clydindgp1130
authored andcommitted
refactor(@angular-devkit/build-angular): preprocess Sass resolve cache entries in esbuild Sass plugin
When performing Sass import resolution within the experimental esbuild-based browser application builder, the directory cache entries will now be preprocessed during the first directory search operation. This will allow a more streamlined search operation on later resolution attempts for the same directory. The caching is still limited to sharing per render request but this change provides for cache entries that can more easily be serialized and shared between workers in the future.
1 parent 6ecfcf4 commit bc8578c

File tree

2 files changed

+70
-37
lines changed

2 files changed

+70
-37
lines changed

packages/angular_devkit/build_angular/src/sass/rebasing-importer.ts

+68-35
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import { RawSourceMap } from '@ampproject/remapping';
1010
import MagicString from 'magic-string';
11-
import { Dirent, readFileSync, readdirSync } from 'node:fs';
11+
import { readFileSync, readdirSync } from 'node:fs';
1212
import { basename, dirname, extname, join, relative } from 'node:path';
1313
import { fileURLToPath, pathToFileURL } from 'node:url';
1414
import type { FileImporter, Importer, ImporterResult, Syntax } from 'sass';
@@ -19,6 +19,15 @@ import type { FileImporter, Importer, ImporterResult, Syntax } from 'sass';
1919
*/
2020
const URL_REGEXP = /url(?:\(\s*(['"]?))(.*?)(?:\1\s*\))/g;
2121

22+
/**
23+
* A preprocessed cache entry for the files and directories within a previously searched
24+
* directory when performing Sass import resolution.
25+
*/
26+
export interface DirectoryEntry {
27+
files: Set<string>;
28+
directories: Set<string>;
29+
}
30+
2231
/**
2332
* A Sass Importer base class that provides the load logic to rebase all `url()` functions
2433
* within a stylesheet. The rebasing will ensure that the URLs in the output of the Sass compiler
@@ -115,7 +124,7 @@ abstract class UrlRebasingImporter implements Importer<'sync'> {
115124
export class RelativeUrlRebasingImporter extends UrlRebasingImporter {
116125
constructor(
117126
entryDirectory: string,
118-
private directoryCache = new Map<string, Dirent[]>(),
127+
private directoryCache = new Map<string, DirectoryEntry>(),
119128
rebaseSourceMaps?: Map<string, RawSourceMap>,
120129
) {
121130
super(entryDirectory, rebaseSourceMaps);
@@ -149,17 +158,6 @@ export class RelativeUrlRebasingImporter extends UrlRebasingImporter {
149158
// Remove the style extension if present to allow adding the `.import` suffix
150159
const filename = basename(stylesheetPath, hasStyleExtension ? extension : undefined);
151160

152-
let entries;
153-
try {
154-
entries = this.directoryCache.get(directory);
155-
if (!entries) {
156-
entries = readdirSync(directory, { withFileTypes: true });
157-
this.directoryCache.set(directory, entries);
158-
}
159-
} catch {
160-
return null;
161-
}
162-
163161
const importPotentials = new Set<string>();
164162
const defaultPotentials = new Set<string>();
165163

@@ -187,47 +185,82 @@ export class RelativeUrlRebasingImporter extends UrlRebasingImporter {
187185
defaultPotentials.add('_' + filename + '.css');
188186
}
189187

190-
const foundDefaults: string[] = [];
191-
const foundImports: string[] = [];
188+
let foundDefaults;
189+
let foundImports;
192190
let hasPotentialIndex = false;
193-
for (const entry of entries) {
194-
// Record if the name should be checked as a directory with an index file
195-
if (checkDirectory && !hasStyleExtension && entry.name === filename && entry.isDirectory()) {
196-
hasPotentialIndex = true;
197-
}
198191

199-
if (!entry.isFile()) {
200-
continue;
192+
let cachedEntries = this.directoryCache.get(directory);
193+
if (cachedEntries) {
194+
// If there is a preprocessed cache of the directory, perform an intersection of the potentials
195+
// and the directory files.
196+
const { files, directories } = cachedEntries;
197+
foundDefaults = [...defaultPotentials].filter((potential) => files.has(potential));
198+
foundImports = [...importPotentials].filter((potential) => files.has(potential));
199+
hasPotentialIndex = checkDirectory && !hasStyleExtension && directories.has(filename);
200+
} else {
201+
// If no preprocessed cache exists, get the entries from the file system and, while searching,
202+
// generate the cache for later requests.
203+
let entries;
204+
try {
205+
entries = readdirSync(directory, { withFileTypes: true });
206+
} catch {
207+
return null;
201208
}
202209

203-
if (importPotentials.has(entry.name)) {
204-
foundImports.push(join(directory, entry.name));
205-
}
210+
foundDefaults = [];
211+
foundImports = [];
212+
cachedEntries = { files: new Set<string>(), directories: new Set<string>() };
213+
for (const entry of entries) {
214+
const isDirectory = entry.isDirectory();
215+
if (isDirectory) {
216+
cachedEntries.directories.add(entry.name);
217+
}
218+
219+
// Record if the name should be checked as a directory with an index file
220+
if (checkDirectory && !hasStyleExtension && entry.name === filename && isDirectory) {
221+
hasPotentialIndex = true;
222+
}
223+
224+
if (!entry.isFile()) {
225+
continue;
226+
}
206227

207-
if (defaultPotentials.has(entry.name)) {
208-
foundDefaults.push(join(directory, entry.name));
228+
cachedEntries.files.add(entry.name);
229+
230+
if (importPotentials.has(entry.name)) {
231+
foundImports.push(entry.name);
232+
}
233+
234+
if (defaultPotentials.has(entry.name)) {
235+
foundDefaults.push(entry.name);
236+
}
209237
}
238+
239+
this.directoryCache.set(directory, cachedEntries);
210240
}
211241

212242
// `foundImports` will only contain elements if `options.fromImport` is true
213243
const result = this.checkFound(foundImports) ?? this.checkFound(foundDefaults);
244+
if (result !== null) {
245+
return pathToFileURL(join(directory, result));
246+
}
214247

215-
if (result === null && hasPotentialIndex) {
248+
if (hasPotentialIndex) {
216249
// Check for index files using filename as a directory
217250
return this.resolveImport(url + '/index', fromImport, false);
218251
}
219252

220-
return result;
253+
return null;
221254
}
222255

223256
/**
224257
* Checks an array of potential stylesheet files to determine if there is a valid
225258
* stylesheet file. More than one discovered file may indicate an error.
226259
* @param found An array of discovered stylesheet files.
227-
* @returns A fully resolved URL for a stylesheet file or `null` if not found.
260+
* @returns A fully resolved path for a stylesheet file or `null` if not found.
228261
* @throws If there are ambiguous files discovered.
229262
*/
230-
private checkFound(found: string[]): URL | null {
263+
private checkFound(found: string[]): string | null {
231264
if (found.length === 0) {
232265
// Not found
233266
return null;
@@ -245,10 +278,10 @@ export class RelativeUrlRebasingImporter extends UrlRebasingImporter {
245278

246279
// Return the non-CSS file (sass/scss files have priority)
247280
// https://github.com/sass/dart-sass/blob/44d6bb6ac72fe6b93f5bfec371a1fffb18e6b76d/lib/src/importer/utils.dart#L44-L47
248-
return pathToFileURL(foundWithoutCss[0]);
281+
return foundWithoutCss[0];
249282
}
250283

251-
return pathToFileURL(found[0]);
284+
return found[0];
252285
}
253286
}
254287

@@ -260,7 +293,7 @@ export class RelativeUrlRebasingImporter extends UrlRebasingImporter {
260293
export class ModuleUrlRebasingImporter extends RelativeUrlRebasingImporter {
261294
constructor(
262295
entryDirectory: string,
263-
directoryCache: Map<string, Dirent[]>,
296+
directoryCache: Map<string, DirectoryEntry>,
264297
rebaseSourceMaps: Map<string, RawSourceMap> | undefined,
265298
private finder: FileImporter<'sync'>['findFileUrl'],
266299
) {
@@ -286,7 +319,7 @@ export class ModuleUrlRebasingImporter extends RelativeUrlRebasingImporter {
286319
export class LoadPathsUrlRebasingImporter extends RelativeUrlRebasingImporter {
287320
constructor(
288321
entryDirectory: string,
289-
directoryCache: Map<string, Dirent[]>,
322+
directoryCache: Map<string, DirectoryEntry>,
290323
rebaseSourceMaps: Map<string, RawSourceMap> | undefined,
291324
private loadPaths: Iterable<string>,
292325
) {

packages/angular_devkit/build_angular/src/sass/worker.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
*/
88

99
import mergeSourceMaps, { RawSourceMap } from '@ampproject/remapping';
10-
import { Dirent } from 'node:fs';
1110
import { dirname } from 'node:path';
1211
import { fileURLToPath, pathToFileURL } from 'node:url';
1312
import { MessagePort, parentPort, receiveMessageOnPort, workerData } from 'node:worker_threads';
@@ -19,6 +18,7 @@ import {
1918
compileString,
2019
} from 'sass';
2120
import {
21+
DirectoryEntry,
2222
LoadPathsUrlRebasingImporter,
2323
ModuleUrlRebasingImporter,
2424
RelativeUrlRebasingImporter,
@@ -82,7 +82,7 @@ parentPort.on('message', (message: RenderRequestMessage) => {
8282
}[]
8383
| undefined;
8484
try {
85-
const directoryCache = new Map<string, Dirent[]>();
85+
const directoryCache = new Map<string, DirectoryEntry>();
8686
const rebaseSourceMaps = options.sourceMap ? new Map<string, RawSourceMap>() : undefined;
8787
if (hasImporter) {
8888
// When a custom importer function is present, the importer request must be proxied

0 commit comments

Comments
 (0)