diff --git a/packages/angular/build/src/builders/application/tests/options/assets_spec.ts b/packages/angular/build/src/builders/application/tests/options/assets_spec.ts
index 96ae3c0d943e..573711afe3b2 100644
--- a/packages/angular/build/src/builders/application/tests/options/assets_spec.ts
+++ b/packages/angular/build/src/builders/application/tests/options/assets_spec.ts
@@ -107,19 +107,19 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
harness.expectFile('dist/browser/test.svg').toNotExist();
});
- it('fail if asset path is not within project source root', async () => {
- await harness.writeFile('test.svg', '');
+ it('copies an asset from project root (outside source root)', async () => {
+ await harness.writeFile('extra.txt', 'extra');
harness.useTarget('build', {
...BASE_OPTIONS,
- assets: ['test.svg'],
+ assets: ['extra.txt'],
});
- const { error } = await harness.executeOnce({ outputLogsOnException: false });
+ const { result } = await harness.executeOnce();
- expect(error?.message).toMatch('path must start with the project source root');
+ expect(result?.success).toBe(true);
- harness.expectFile('dist/browser/test.svg').toNotExist();
+ harness.expectFile('dist/browser/extra.txt').content.toBe('extra');
});
});
@@ -359,6 +359,17 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
harness.expectFile('dist/browser/subdirectory/test.svg').content.toBe('');
});
+ it('fails if asset path is outside workspace root', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: ['../outside.txt'],
+ });
+
+ const { error } = await harness.executeOnce({ outputLogsOnException: false });
+
+ expect(error?.message).toMatch('asset path must be within the workspace root');
+ });
+
it('fails if output option is not within project output path', async () => {
await harness.writeFile('test.svg', '');
diff --git a/packages/angular/build/src/utils/normalize-asset-patterns.ts b/packages/angular/build/src/utils/normalize-asset-patterns.ts
index 8a8b2c2cbf1f..929e88fff506 100644
--- a/packages/angular/build/src/utils/normalize-asset-patterns.ts
+++ b/packages/angular/build/src/utils/normalize-asset-patterns.ts
@@ -11,12 +11,6 @@ import { statSync } from 'node:fs';
import * as path from 'node:path';
import { AssetPattern, AssetPatternClass } from '../builders/application/schema';
-export class MissingAssetSourceRootException extends Error {
- constructor(path: string) {
- super(`The ${path} asset path must start with the project source root.`);
- }
-}
-
export function normalizeAssetPatterns(
assetPatterns: AssetPattern[],
workspaceRoot: string,
@@ -30,16 +24,24 @@ export function normalizeAssetPatterns(
// When sourceRoot is not available, we default to ${projectRoot}/src.
const sourceRoot = projectSourceRoot || path.join(projectRoot, 'src');
const resolvedSourceRoot = path.resolve(workspaceRoot, sourceRoot);
+ const resolvedProjectRoot = path.resolve(workspaceRoot, projectRoot);
return assetPatterns.map((assetPattern) => {
// Normalize string asset patterns to objects.
if (typeof assetPattern === 'string') {
const assetPath = path.normalize(assetPattern);
const resolvedAssetPath = path.resolve(workspaceRoot, assetPath);
+ let root: string;
// Check if the string asset is within sourceRoot.
- if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) {
- throw new MissingAssetSourceRootException(assetPattern);
+ if (resolvedAssetPath.startsWith(resolvedSourceRoot)) {
+ root = resolvedSourceRoot;
+ } else if (resolvedAssetPath.startsWith(resolvedProjectRoot)) {
+ root = resolvedProjectRoot;
+ } else if (resolvedAssetPath.startsWith(workspaceRoot)) {
+ root = workspaceRoot;
+ } else {
+ throw new Error(`The ${assetPattern} asset path must be within the workspace root.`);
}
let glob: string, input: string;
@@ -63,8 +65,8 @@ export function normalizeAssetPatterns(
input = path.dirname(assetPath);
}
- // Output directory for both is the relative path from source root to input.
- const output = path.relative(resolvedSourceRoot, path.resolve(workspaceRoot, input));
+ // Output directory for both is the relative path from the root to input.
+ const output = path.relative(root, path.resolve(workspaceRoot, input));
assetPattern = { glob, input, output };
} else {
diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/assets_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/assets_spec.ts
index 740612d19478..1493b55172a8 100644
--- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/assets_spec.ts
+++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/assets_spec.ts
@@ -106,21 +106,6 @@ describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => {
harness.expectFile('dist/test.svg').toNotExist();
});
-
- it('fail if asset path is not within project source root', async () => {
- await harness.writeFile('test.svg', '');
-
- harness.useTarget('build', {
- ...BASE_OPTIONS,
- assets: ['test.svg'],
- });
-
- const { error } = await harness.executeOnce({ outputLogsOnException: false });
-
- expect(error?.message).toMatch('path must start with the project source root');
-
- harness.expectFile('dist/test.svg').toNotExist();
- });
});
describe('longhand syntax', () => {