Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/angular/build/src/builders/library/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});

Expand All @@ -25,6 +26,7 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) =>
glob: '**/*.png',
input: 'projects/lib/assets-dir',
output: 'assets',
ignore: ['ignored.png'],
},
],
});
Expand All @@ -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();
});

Expand Down
64 changes: 35 additions & 29 deletions packages/angular/build/src/utils/normalize-asset-patterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 [];
}
Expand All @@ -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);
Expand All @@ -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;
});
}
Loading