From 280984481c338474e824d60dffff787b4f6546cd Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:50:57 +0000 Subject: [PATCH] refactor(@angular/build): export NormalizedAssetsPattern and avoid mutating input patterns Export the `NormalizedAssetsPattern` interface from `normalize-asset-patterns.ts`, avoid mutating input `AssetPatternClass` objects in place, and simplify directory detection using `statSync(..., { throwIfNoEntry: false })`. Also import `normalizeAssetPatterns` directly in `library/options.ts` and add test coverage for asset `ignore` patterns. --- .../build/src/builders/library/options.ts | 2 +- .../library/tests/options/assets_spec.ts | 3 + .../src/utils/normalize-asset-patterns.ts | 64 ++++++++++--------- 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/packages/angular/build/src/builders/library/options.ts b/packages/angular/build/src/builders/library/options.ts index 14bc4514027d..23d18d6f4d8e 100644 --- a/packages/angular/build/src/builders/library/options.ts +++ b/packages/angular/build/src/builders/library/options.ts @@ -9,9 +9,9 @@ import type { BuilderContext } from '@angular-devkit/architect'; import path from 'node:path'; import type { StylesheetPluginsass } from '../../tools/esbuild/stylesheets/stylesheet-plugin-factory'; -import { normalizeAssetPatterns } from '../../utils'; import { supportColor } from '../../utils/color'; import { assertIsError } from '../../utils/error'; +import { normalizeAssetPatterns } from '../../utils/normalize-asset-patterns'; import { normalizeCacheOptions } from '../../utils/normalize-cache'; import { toPosixPath } from '../../utils/path'; import { diff --git a/packages/angular/build/src/builders/library/tests/options/assets_spec.ts b/packages/angular/build/src/builders/library/tests/options/assets_spec.ts index ded7743675ff..a596cbfe4e62 100644 --- a/packages/angular/build/src/builders/library/tests/options/assets_spec.ts +++ b/packages/angular/build/src/builders/library/tests/options/assets_spec.ts @@ -15,6 +15,7 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => await harness.writeFiles({ 'projects/lib/assets-dir/file-a.png': 'PNG_A', 'projects/lib/assets-dir/file-b.png': 'PNG_B', + 'projects/lib/assets-dir/ignored.png': 'PNG_IGNORED', 'projects/lib/assets-dir/file-c.svg': 'SVG_C', }); @@ -25,6 +26,7 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => glob: '**/*.png', input: 'projects/lib/assets-dir', output: 'assets', + ignore: ['ignored.png'], }, ], }); @@ -34,6 +36,7 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => expect(harness.readFile('dist/lib/assets/file-a.png')).toBe('PNG_A'); expect(harness.readFile('dist/lib/assets/file-b.png')).toBe('PNG_B'); + expect(harness.hasFile('dist/lib/assets/ignored.png')).toBeFalse(); expect(harness.hasFile('dist/lib/assets/file-c.svg')).toBeFalse(); }); diff --git a/packages/angular/build/src/utils/normalize-asset-patterns.ts b/packages/angular/build/src/utils/normalize-asset-patterns.ts index 66365e10df22..defa6511172f 100644 --- a/packages/angular/build/src/utils/normalize-asset-patterns.ts +++ b/packages/angular/build/src/utils/normalize-asset-patterns.ts @@ -6,18 +6,35 @@ * found in the LICENSE file at https://angular.dev/license */ -import assert from 'node:assert'; import { statSync } from 'node:fs'; -import * as path from 'node:path'; -import { AssetPattern, AssetPatternClass } from '../builders/application/schema'; +import path from 'node:path'; +import type { AssetPattern, AssetPatternClass } from '../builders/application/schema'; import { isSubDirectory } from './path'; +/** + * Asset pattern configuration normalized into its object form with a resolved `output` directory. + */ +export interface NormalizedAssetsPattern extends AssetPatternClass { + /** Relative output directory within the build output path. */ + output: string; +} + +/** + * Normalizes raw string and object asset patterns into {@link NormalizedAssetsPattern} objects + * and validates that all asset inputs and outputs stay within the workspace and output directories. + * + * @param assetPatterns Raw string or object asset patterns from builder options. + * @param workspaceRoot Workspace root directory path. + * @param projectRoot Project root directory path. + * @param projectSourceRoot Optional project source root directory path. + * @returns An array of normalized asset pattern configurations. + */ export function normalizeAssetPatterns( assetPatterns: AssetPattern[], workspaceRoot: string, projectRoot: string, projectSourceRoot: string | undefined, -): (AssetPatternClass & { output: string })[] { +): NormalizedAssetsPattern[] { if (assetPatterns.length === 0) { return []; } @@ -29,6 +46,8 @@ export function normalizeAssetPatterns( const resolvedProjectRoot = path.resolve(resolvedWorkspaceRoot, projectRoot); return assetPatterns.map((assetPattern) => { + let normalized: NormalizedAssetsPattern; + // Normalize string asset patterns to objects. if (typeof assetPattern === 'string') { const assetPath = path.normalize(assetPattern); @@ -46,45 +65,32 @@ export function normalizeAssetPatterns( throw new Error(`The ${assetPattern} asset path must be within the workspace root.`); } - let glob: string, input: string; - let isDirectory: boolean; - + let isDirectory = true; try { - isDirectory = statSync(resolvedAssetPath).isDirectory(); - } catch { - isDirectory = true; - } - - if (isDirectory) { - // Folders get a recursive star glob. - glob = '**/*'; - // Input directory is their original path. - input = assetPath; - } else { - // Files are their own glob. - glob = path.basename(assetPath); - // Input directory is their original dirname. - input = path.dirname(assetPath); - } + isDirectory = statSync(resolvedAssetPath, { throwIfNoEntry: false })?.isDirectory() ?? true; + } catch {} + const glob = isDirectory ? '**/*' : path.basename(assetPath); + const input = isDirectory ? assetPath : path.dirname(assetPath); // Output directory for both is the relative path from the root to input. const output = path.relative(root, path.resolve(resolvedWorkspaceRoot, input)); - assetPattern = { glob, input, output }; + normalized = { glob, input, output }; } else { if (!isSubDirectory(resolvedWorkspaceRoot, assetPattern.input)) { throw new Error(`The ${assetPattern.input} asset path must be within the workspace root.`); } - assetPattern.output = path.join('.', assetPattern.output ?? ''); + normalized = { + ...assetPattern, + output: path.join('.', assetPattern.output ?? ''), + }; } - assert(assetPattern.output !== undefined); - - if (assetPattern.output.startsWith('..')) { + if (normalized.output.startsWith('..')) { throw new Error('An asset cannot be written to a location outside of the output path.'); } - return assetPattern as AssetPatternClass & { output: string }; + return normalized; }); }