diff --git a/packages/angular/build/src/tools/esbuild/persistent-load-result-cache.ts b/packages/angular/build/src/tools/esbuild/persistent-load-result-cache.ts index c5dcee90ccf9..7eaf11806f1d 100644 --- a/packages/angular/build/src/tools/esbuild/persistent-load-result-cache.ts +++ b/packages/angular/build/src/tools/esbuild/persistent-load-result-cache.ts @@ -32,7 +32,7 @@ import { readFile, stat } from 'node:fs/promises'; import { isAbsolute } from 'node:path'; import { fileURLToPath } from 'node:url'; import type { Cache as PersistentCacheStore } from '../../utils/cache'; -import { mapConcurrent, runConcurrent } from '../../utils/concurrency'; +import { MAX_CONCURRENT_READS, mapConcurrent, runConcurrent } from '../../utils/concurrency'; import { calculateHash, createContentHash } from '../../utils/hash'; import { LoadResultCache, MemoryLoadResultCache } from './load-result-cache'; @@ -112,9 +112,6 @@ export function extractDiskFilePath(path: string): string | undefined { return isAbsolute(path) ? path : undefined; } -/** Maximum number of concurrent file system read/stat operations to prevent OS file descriptor exhaustion. */ -const MAX_CONCURRENT_READS = 16; - /** * Validates that all imported watch files exist on disk and their contents match. * Performs a fast-path metadata check (mtime + size) first, falling back to content hashing. diff --git a/packages/angular/build/src/utils/concurrency.ts b/packages/angular/build/src/utils/concurrency.ts index a38a9a57fa36..39f2b70c95df 100644 --- a/packages/angular/build/src/utils/concurrency.ts +++ b/packages/angular/build/src/utils/concurrency.ts @@ -6,6 +6,11 @@ * found in the LICENSE file at https://angular.dev/license */ +/** + * Maximum number of concurrent file system read/stat operations to prevent OS file descriptor exhaustion. + */ +export const MAX_CONCURRENT_READS = 16; + /** * Executes an asynchronous function for each item in an array concurrently up to a specified limit. * diff --git a/packages/angular/build/src/utils/resolve-assets.ts b/packages/angular/build/src/utils/resolve-assets.ts index 79ddd8548266..06a6c6c157e9 100644 --- a/packages/angular/build/src/utils/resolve-assets.ts +++ b/packages/angular/build/src/utils/resolve-assets.ts @@ -6,8 +6,10 @@ * found in the LICENSE file at https://angular.dev/license */ +import { lstat, stat } from 'node:fs/promises'; import path from 'node:path'; -import { glob } from 'tinyglobby'; +import { glob, isDynamicPattern } from 'tinyglobby'; +import { MAX_CONCURRENT_READS, mapConcurrent } from './concurrency'; import { isSubDirectory } from './path'; /** @@ -26,15 +28,34 @@ export async function resolveAssets( }[], root: string, ): Promise<{ source: string; destination: string }[]> { - const outputFiles: { source: string; destination: string }[] = []; - - for (const entry of entries) { + const resolvedEntries = await mapConcurrent(entries, MAX_CONCURRENT_READS, async (entry) => { if (!isSubDirectory(root, entry.input)) { throw new Error(`The ${entry.input} asset path must be within the workspace root.`); } const cwd = path.resolve(root, entry.input); + if (!entry.ignore?.length && !isDynamicPattern(entry.glob)) { + const src = path.join(cwd, entry.glob); + if (isSubDirectory(cwd, src)) { + try { + const stats = await (entry.followSymlinks ? stat(src) : lstat(src)); + if (stats.isFile()) { + const filePath = entry.flatten ? path.basename(entry.glob) : entry.glob; + + return [{ source: src, destination: path.join(entry.output, filePath) }]; + } + + if (!stats.isSymbolicLink()) { + return []; + } + } catch { + // File does not exist or cannot be accessed. + return []; + } + } + } + const files = await glob(entry.glob, { cwd, dot: true, @@ -42,13 +63,11 @@ export async function resolveAssets( followSymbolicLinks: entry.followSymlinks ?? false, }); - for (const file of files) { - const src = path.join(cwd, file); - const filePath = entry.flatten ? path.basename(file) : file; - - outputFiles.push({ source: src, destination: path.join(entry.output, filePath) }); - } - } + return files.map((file) => ({ + source: path.join(cwd, file), + destination: path.join(entry.output, entry.flatten ? path.basename(file) : file), + })); + }); - return outputFiles; + return resolvedEntries.flat(); }