From 4784fe7e8629c9b48479a728d16a59b509f32b53 Mon Sep 17 00:00:00 2001
From: Charles Lyding <19598772+clydin@users.noreply.github.com>
Date: Thu, 15 Jan 2026 15:24:11 -0500
Subject: [PATCH] fix(@angular/build): allow application assets in workspace
root
This commit refactors `normalizeAssetPatterns` to:
1. Allow asset paths located in the workspace root or project root, relaxing the previous strict requirement of being within the source root.
2. Determine the output path by calculating the relative path from the most specific root (source, project, or workspace) to the asset input.
3. Remove the unused `MissingAssetSourceRootException` class in favor of a standard `Error` with a clear message.
This enables users to include workspace-level assets (like `LICENSE` or `README.md`) using the shorthand string syntax without errors.
---
.../application/tests/options/assets_spec.ts | 23 ++++++++++++++-----
.../src/utils/normalize-asset-patterns.ts | 22 ++++++++++--------
.../tests/options/assets_spec.ts | 15 ------------
3 files changed, 29 insertions(+), 31 deletions(-)
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', () => {