diff --git a/goldens/public-api/angular_devkit/build_angular/index.md b/goldens/public-api/angular_devkit/build_angular/index.md
index a5e253c337a2..2a53da0543bd 100644
--- a/goldens/public-api/angular_devkit/build_angular/index.md
+++ b/goldens/public-api/angular_devkit/build_angular/index.md
@@ -246,6 +246,7 @@ export interface ProtractorBuilderOptions {
// @public (undocumented)
export interface ServerBuilderOptions {
+ assets?: AssetPattern_3[];
deleteOutputPath?: boolean;
// @deprecated
deployUrl?: string;
diff --git a/packages/angular_devkit/build_angular/src/builders/browser/index.ts b/packages/angular_devkit/build_angular/src/builders/browser/index.ts
index 30bf61c8896c..c7361ffdb311 100644
--- a/packages/angular_devkit/build_angular/src/builders/browser/index.ts
+++ b/packages/angular_devkit/build_angular/src/builders/browser/index.ts
@@ -111,20 +111,6 @@ async function initialize(
getStylesConfig(wco),
]);
- // Validate asset option values if processed directly
- if (options.assets?.length && !adjustedOptions.assets?.length) {
- normalizeAssetPatterns(
- options.assets,
- context.workspaceRoot,
- projectRoot,
- projectSourceRoot,
- ).forEach(({ output }) => {
- if (output.startsWith('..')) {
- throw new Error('An asset cannot be written to a location outside of the output path.');
- }
- });
- }
-
let transformedConfig;
if (webpackConfigurationTransform) {
transformedConfig = await webpackConfigurationTransform(config);
diff --git a/packages/angular_devkit/build_angular/src/builders/browser/tests/options/assets_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/tests/options/assets_spec.ts
index 0029b6b1e8ca..16394d1f7004 100644
--- a/packages/angular_devkit/build_angular/src/builders/browser/tests/options/assets_spec.ts
+++ b/packages/angular_devkit/build_angular/src/builders/browser/tests/options/assets_spec.ts
@@ -107,7 +107,7 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
harness.expectFile('dist/test.svg').toNotExist();
});
- it('throws exception if asset path is not within project source root', async () => {
+ it('fail if asset path is not within project source root', async () => {
await harness.writeFile('test.svg', '');
harness.useTarget('build', {
@@ -115,14 +115,9 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
assets: ['test.svg'],
});
- const { result, error } = await harness.executeOnce({ outputLogsOnException: false });
+ const { result } = await harness.executeOnce();
- expect(result).toBeUndefined();
- expect(error).toEqual(
- jasmine.objectContaining({
- message: jasmine.stringMatching('path must start with the project source root'),
- }),
- );
+ expect(result?.error).toMatch('path must start with the project source root');
harness.expectFile('dist/test.svg').toNotExist();
});
@@ -364,7 +359,7 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
harness.expectFile('dist/subdirectory/test.svg').content.toBe('');
});
- it('throws exception if output option is not within project output path', async () => {
+ it('fails if output option is not within project output path', async () => {
await harness.writeFile('test.svg', '');
harness.useTarget('build', {
@@ -372,15 +367,10 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
assets: [{ glob: 'test.svg', input: 'src', output: '..' }],
});
- const { result, error } = await harness.executeOnce({ outputLogsOnException: false });
+ const { result } = await harness.executeOnce();
- expect(result).toBeUndefined();
- expect(error).toEqual(
- jasmine.objectContaining({
- message: jasmine.stringMatching(
- 'An asset cannot be written to a location outside of the output path',
- ),
- }),
+ expect(result?.error).toMatch(
+ 'An asset cannot be written to a location outside of the output path',
);
harness.expectFile('dist/test.svg').toNotExist();
diff --git a/packages/angular_devkit/build_angular/src/builders/server/index.ts b/packages/angular_devkit/build_angular/src/builders/server/index.ts
index dd38c82af7d2..011a42088805 100644
--- a/packages/angular_devkit/build_angular/src/builders/server/index.ts
+++ b/packages/angular_devkit/build_angular/src/builders/server/index.ts
@@ -10,14 +10,22 @@ import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/ar
import { runWebpack } from '@angular-devkit/build-webpack';
import * as path from 'path';
import { Observable, from } from 'rxjs';
-import { concatMap, map } from 'rxjs/operators';
+import { concatMap } from 'rxjs/operators';
import webpack, { Configuration } from 'webpack';
import { ExecutionTransformer } from '../../transforms';
-import { NormalizedBrowserBuilderSchema, deleteOutputDir } from '../../utils';
+import {
+ NormalizedBrowserBuilderSchema,
+ deleteOutputDir,
+ normalizeAssetPatterns,
+} from '../../utils';
+import { colors } from '../../utils/color';
+import { copyAssets } from '../../utils/copy-assets';
+import { assertIsError } from '../../utils/error';
import { i18nInlineEmittedFiles } from '../../utils/i18n-inlining';
import { I18nOptions } from '../../utils/i18n-options';
import { ensureOutputPaths } from '../../utils/output-paths';
import { purgeStaleBuildCache } from '../../utils/purge-cache';
+import { Spinner } from '../../utils/spinner';
import { assertCompatibleAngularVersion } from '../../utils/version';
import {
BrowserWebpackConfigOptions,
@@ -69,7 +77,7 @@ export function execute(
let outputPaths: undefined | Map;
return from(initialize(options, context, transforms.webpackConfiguration)).pipe(
- concatMap(({ config, i18n }) => {
+ concatMap(({ config, i18n, projectRoot, projectSourceRoot }) => {
return runWebpack(config, context, {
webpackFactory: require('webpack') as typeof webpack,
logging: (stats, config) => {
@@ -84,11 +92,43 @@ export function execute(
throw new Error('Webpack stats build result is required.');
}
- let success = output.success;
- if (success && i18n.shouldInline) {
- outputPaths = ensureOutputPaths(baseOutputPath, i18n);
+ if (!output.success) {
+ return output;
+ }
- success = await i18nInlineEmittedFiles(
+ const spinner = new Spinner();
+ spinner.enabled = options.progress !== false;
+ outputPaths = ensureOutputPaths(baseOutputPath, i18n);
+
+ // Copy assets
+ if (!options.watch && options.assets?.length) {
+ spinner.start('Copying assets...');
+ try {
+ await copyAssets(
+ normalizeAssetPatterns(
+ options.assets,
+ context.workspaceRoot,
+ projectRoot,
+ projectSourceRoot,
+ ),
+ Array.from(outputPaths.values()),
+ context.workspaceRoot,
+ );
+ spinner.succeed('Copying assets complete.');
+ } catch (err) {
+ spinner.fail(colors.redBright('Copying of assets failed.'));
+ assertIsError(err);
+
+ return {
+ ...output,
+ success: false,
+ error: 'Unable to copy assets: ' + err.message,
+ };
+ }
+ }
+
+ if (i18n.shouldInline) {
+ const success = await i18nInlineEmittedFiles(
context,
emittedFiles,
i18n,
@@ -98,15 +138,21 @@ export function execute(
outputPath,
options.i18nMissingTranslation,
);
+ if (!success) {
+ return {
+ ...output,
+ success: false,
+ };
+ }
}
webpackStatsLogger(context.logger, webpackStats, config);
- return { ...output, success };
+ return output;
}),
);
}),
- map((output) => {
+ concatMap(async (output) => {
if (!output.success) {
return output as ServerBuilderOutput;
}
@@ -137,28 +183,34 @@ async function initialize(
): Promise<{
config: webpack.Configuration;
i18n: I18nOptions;
+ projectRoot: string;
+ projectSourceRoot?: string;
}> {
// Purge old build disk cache.
await purgeStaleBuildCache(context);
const browserslist = (await import('browserslist')).default;
const originalOutputPath = options.outputPath;
- const { config, i18n } = await generateI18nBrowserWebpackConfigFromContext(
- {
- ...options,
- buildOptimizer: false,
- aot: true,
- platform: 'server',
- } as NormalizedBrowserBuilderSchema,
- context,
- (wco) => {
- // We use the platform to determine the JavaScript syntax output.
- wco.buildOptions.supportedBrowsers ??= [];
- wco.buildOptions.supportedBrowsers.push(...browserslist('maintained node versions'));
-
- return [getPlatformServerExportsConfig(wco), getCommonConfig(wco), getStylesConfig(wco)];
- },
- );
+ // Assets are processed directly by the builder except when watching
+ const adjustedOptions = options.watch ? options : { ...options, assets: [] };
+
+ const { config, projectRoot, projectSourceRoot, i18n } =
+ await generateI18nBrowserWebpackConfigFromContext(
+ {
+ ...adjustedOptions,
+ buildOptimizer: false,
+ aot: true,
+ platform: 'server',
+ } as NormalizedBrowserBuilderSchema,
+ context,
+ (wco) => {
+ // We use the platform to determine the JavaScript syntax output.
+ wco.buildOptions.supportedBrowsers ??= [];
+ wco.buildOptions.supportedBrowsers.push(...browserslist('maintained node versions'));
+
+ return [getPlatformServerExportsConfig(wco), getCommonConfig(wco), getStylesConfig(wco)];
+ },
+ );
if (options.deleteOutputPath) {
deleteOutputDir(context.workspaceRoot, originalOutputPath);
@@ -166,7 +218,7 @@ async function initialize(
const transformedConfig = (await webpackConfigurationTransform?.(config)) ?? config;
- return { config: transformedConfig, i18n };
+ return { config: transformedConfig, i18n, projectRoot, projectSourceRoot };
}
/**
diff --git a/packages/angular_devkit/build_angular/src/builders/server/schema.json b/packages/angular_devkit/build_angular/src/builders/server/schema.json
index 73d6088c1f38..8506cff635b3 100644
--- a/packages/angular_devkit/build_angular/src/builders/server/schema.json
+++ b/packages/angular_devkit/build_angular/src/builders/server/schema.json
@@ -4,6 +4,14 @@
"title": "Universal Target",
"type": "object",
"properties": {
+ "assets": {
+ "type": "array",
+ "description": "List of static application assets.",
+ "default": [],
+ "items": {
+ "$ref": "#/definitions/assetPattern"
+ }
+ },
"main": {
"type": "string",
"description": "The name of the main entry-point file."
@@ -212,6 +220,44 @@
"additionalProperties": false,
"required": ["outputPath", "main", "tsConfig"],
"definitions": {
+ "assetPattern": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "followSymlinks": {
+ "type": "boolean",
+ "default": false,
+ "description": "Allow glob patterns to follow symlink directories. This allows subdirectories of the symlink to be searched."
+ },
+ "glob": {
+ "type": "string",
+ "description": "The pattern to match."
+ },
+ "input": {
+ "type": "string",
+ "description": "The input directory path in which to apply 'glob'. Defaults to the project root."
+ },
+ "ignore": {
+ "description": "An array of globs to ignore.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "output": {
+ "type": "string",
+ "description": "Absolute path within the output."
+ }
+ },
+ "additionalProperties": false,
+ "required": ["glob", "input", "output"]
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
"fileReplacement": {
"oneOf": [
{
diff --git a/packages/angular_devkit/build_angular/src/builders/server/tests/options/assets_spec.ts b/packages/angular_devkit/build_angular/src/builders/server/tests/options/assets_spec.ts
new file mode 100644
index 000000000000..1a79f6b79d6a
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/builders/server/tests/options/assets_spec.ts
@@ -0,0 +1,382 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import { execute } from '../../index';
+import { BASE_OPTIONS, SERVER_BUILDER_INFO, describeBuilder } from '../setup';
+
+describeBuilder(execute, SERVER_BUILDER_INFO, (harness) => {
+ describe('Option: "assets"', () => {
+ beforeEach(async () => {
+ // Application code is not needed for asset tests
+ await harness.writeFile('src/main.server.ts', '');
+ });
+
+ it('supports an empty array value', async () => {
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: [],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+ });
+
+ it('supports mixing shorthand and longhand syntax', async () => {
+ await harness.writeFile('src/files/test.svg', '');
+ await harness.writeFile('src/files/another.file', 'asset file');
+ await harness.writeFile('src/extra.file', 'extra file');
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: ['src/extra.file', { glob: '*', input: 'src/files', output: '.' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/extra.file').content.toBe('extra file');
+ harness.expectFile('dist/test.svg').content.toBe('');
+ harness.expectFile('dist/another.file').content.toBe('asset file');
+ });
+
+ describe('shorthand syntax', () => {
+ it('copies a single asset', async () => {
+ await harness.writeFile('src/test.svg', '');
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: ['src/test.svg'],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/test.svg').content.toBe('');
+ });
+
+ it('copies multiple assets', async () => {
+ await harness.writeFile('src/test.svg', '');
+ await harness.writeFile('src/another.file', 'asset file');
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: ['src/test.svg', 'src/another.file'],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/test.svg').content.toBe('');
+ harness.expectFile('dist/another.file').content.toBe('asset file');
+ });
+
+ it('copies an asset with directory and maintains directory in output', async () => {
+ await harness.writeFile('src/subdirectory/test.svg', '');
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: ['src/subdirectory/test.svg'],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/subdirectory/test.svg').content.toBe('');
+ });
+
+ it('does not fail if asset does not exist', async () => {
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: ['src/test.svg'],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/test.svg').toNotExist();
+ });
+
+ it('fails if output option is not within project output path', async () => {
+ await harness.writeFile('test.svg', '');
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: 'test.svg', input: 'src', output: '..' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.error).toMatch(
+ 'An asset cannot be written to a location outside of the output path',
+ );
+
+ harness.expectFile('dist/test.svg').toNotExist();
+ });
+ });
+
+ describe('longhand syntax', () => {
+ it('copies a single asset', async () => {
+ await harness.writeFile('src/test.svg', '');
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: 'test.svg', input: 'src', output: '.' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/test.svg').content.toBe('');
+ });
+
+ it('copies multiple assets as separate entries', async () => {
+ await harness.writeFile('src/test.svg', '');
+ await harness.writeFile('src/another.file', 'asset file');
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: [
+ { glob: 'test.svg', input: 'src', output: '.' },
+ { glob: 'another.file', input: 'src', output: '.' },
+ ],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/test.svg').content.toBe('');
+ harness.expectFile('dist/another.file').content.toBe('asset file');
+ });
+
+ it('copies multiple assets with a single entry glob pattern', async () => {
+ await harness.writeFile('src/test.svg', '');
+ await harness.writeFile('src/another.file', 'asset file');
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: '{test.svg,another.file}', input: 'src', output: '.' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/test.svg').content.toBe('');
+ harness.expectFile('dist/another.file').content.toBe('asset file');
+ });
+
+ it('copies multiple assets with a wildcard glob pattern', async () => {
+ await harness.writeFile('src/files/test.svg', '');
+ await harness.writeFile('src/files/another.file', 'asset file');
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: '*', input: 'src/files', output: '.' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/test.svg').content.toBe('');
+ harness.expectFile('dist/another.file').content.toBe('asset file');
+ });
+
+ it('copies multiple assets with a recursive wildcard glob pattern', async () => {
+ await harness.writeFiles({
+ 'src/files/test.svg': '',
+ 'src/files/another.file': 'asset file',
+ 'src/files/nested/extra.file': 'extra file',
+ });
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: '**/*', input: 'src/files', output: '.' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/test.svg').content.toBe('');
+ harness.expectFile('dist/another.file').content.toBe('asset file');
+ harness.expectFile('dist/nested/extra.file').content.toBe('extra file');
+ });
+
+ it('automatically ignores "." prefixed files when using wildcard glob pattern', async () => {
+ await harness.writeFile('src/files/.gitkeep', '');
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: '*', input: 'src/files', output: '.' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/.gitkeep').toNotExist();
+ });
+
+ it('supports ignoring a specific file when using a glob pattern', async () => {
+ await harness.writeFiles({
+ 'src/files/test.svg': '',
+ 'src/files/another.file': 'asset file',
+ 'src/files/nested/extra.file': 'extra file',
+ });
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: '**/*', input: 'src/files', output: '.', ignore: ['another.file'] }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/test.svg').content.toBe('');
+ harness.expectFile('dist/another.file').toNotExist();
+ harness.expectFile('dist/nested/extra.file').content.toBe('extra file');
+ });
+
+ it('supports ignoring with a glob pattern when using a glob pattern', async () => {
+ await harness.writeFiles({
+ 'src/files/test.svg': '',
+ 'src/files/another.file': 'asset file',
+ 'src/files/nested/extra.file': 'extra file',
+ });
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: '**/*', input: 'src/files', output: '.', ignore: ['**/*.file'] }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/test.svg').content.toBe('');
+ harness.expectFile('dist/another.file').toNotExist();
+ harness.expectFile('dist/nested/extra.file').toNotExist();
+ });
+
+ it('copies an asset with directory and maintains directory in output', async () => {
+ await harness.writeFile('src/subdirectory/test.svg', '');
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: 'subdirectory/test.svg', input: 'src', output: '.' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/subdirectory/test.svg').content.toBe('');
+ });
+
+ it('does not fail if asset does not exist', async () => {
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: 'test.svg', input: 'src', output: '.' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/test.svg').toNotExist();
+ });
+
+ it('uses project output path when output option is empty string', async () => {
+ await harness.writeFile('src/test.svg', '');
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: 'test.svg', input: 'src', output: '' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/test.svg').content.toBe('');
+ });
+
+ it('uses project output path when output option is "."', async () => {
+ await harness.writeFile('src/test.svg', '');
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: 'test.svg', input: 'src', output: '.' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/test.svg').content.toBe('');
+ });
+
+ it('uses project output path when output option is "/"', async () => {
+ await harness.writeFile('src/test.svg', '');
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: 'test.svg', input: 'src', output: '/' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/test.svg').content.toBe('');
+ });
+
+ it('creates a project output sub-path when output option path does not exist', async () => {
+ await harness.writeFile('src/test.svg', '');
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: 'test.svg', input: 'src', output: 'subdirectory' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBeTrue();
+
+ harness.expectFile('dist/subdirectory/test.svg').content.toBe('');
+ });
+
+ it('fails if output option is not within project output path', async () => {
+ await harness.writeFile('test.svg', '');
+
+ harness.useTarget('server', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: 'test.svg', input: 'src', output: '..' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.error).toMatch(
+ 'An asset cannot be written to a location outside of the output path',
+ );
+
+ harness.expectFile('dist/test.svg').toNotExist();
+ });
+ });
+ });
+});
diff --git a/packages/angular_devkit/build_angular/src/builders/server/tests/options/resources-output-path_spec.ts b/packages/angular_devkit/build_angular/src/builders/server/tests/options/resources-output-path_spec.ts
index 581207ce4468..42568d4bfb2e 100644
--- a/packages/angular_devkit/build_angular/src/builders/server/tests/options/resources-output-path_spec.ts
+++ b/packages/angular_devkit/build_angular/src/builders/server/tests/options/resources-output-path_spec.ts
@@ -35,10 +35,10 @@ describeBuilder(execute, SERVER_BUILDER_INFO, (harness) => {
harness
.expectFile('dist/main.js')
- .content.toContain(`url(/assets/component-img-absolute.png)`);
+ .content.toContain(`url('/assets/component-img-absolute.png')`);
harness
.expectFile('dist/main.js')
- .content.toContain(`url(out-assets/component-img-relative.png)`);
+ .content.toContain(`url('out-assets/component-img-relative.png')`);
// Assets are not emitted during a server builds.
harness.expectFile('dist/out-assets/component-img-relative.png').toNotExist();
@@ -54,8 +54,8 @@ describeBuilder(execute, SERVER_BUILDER_INFO, (harness) => {
harness
.expectFile('dist/main.js')
- .content.toContain(`url(/assets/component-img-absolute.png)`);
- harness.expectFile('dist/main.js').content.toContain(`url(component-img-relative.png)`);
+ .content.toContain(`url('/assets/component-img-absolute.png')`);
+ harness.expectFile('dist/main.js').content.toContain(`url('component-img-relative.png')`);
// Assets are not emitted during a server builds.
harness.expectFile('dist/component-img-relative.png').toNotExist();
diff --git a/packages/angular_devkit/build_angular/src/builders/server/tests/setup.ts b/packages/angular_devkit/build_angular/src/builders/server/tests/setup.ts
index e2fa33445afc..21670dc0616d 100644
--- a/packages/angular_devkit/build_angular/src/builders/server/tests/setup.ts
+++ b/packages/angular_devkit/build_angular/src/builders/server/tests/setup.ts
@@ -25,4 +25,7 @@ export const BASE_OPTIONS = Object.freeze({
progress: false,
watch: false,
outputPath: 'dist',
+
+ // Disable optimizations
+ optimization: false,
});
diff --git a/packages/angular_devkit/build_angular/src/utils/normalize-asset-patterns.ts b/packages/angular_devkit/build_angular/src/utils/normalize-asset-patterns.ts
index 02a6529d112c..58c0cf5f09a2 100644
--- a/packages/angular_devkit/build_angular/src/utils/normalize-asset-patterns.ts
+++ b/packages/angular_devkit/build_angular/src/utils/normalize-asset-patterns.ts
@@ -66,11 +66,13 @@ export function normalizeAssetPatterns(
// Output directory for both is the relative path from source root to input.
const output = path.relative(resolvedSourceRoot, path.resolve(workspaceRoot, input));
- // Return the asset pattern in object format.
- return { glob, input, output };
- } else {
- // It's already an AssetPatternObject, no need to convert.
- return assetPattern;
+ assetPattern = { glob, input, output };
}
+
+ if (assetPattern.output.startsWith('..')) {
+ throw new Error('An asset cannot be written to a location outside of the output path.');
+ }
+
+ return assetPattern;
});
}