Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c29df69

Browse files
alan-agius4dgp1130
authored andcommittedNov 18, 2022
feat(@angular-devkit/build-angular): add assets option to server builder
This commits adds the `assets` option to the server builder. This can be useful to copy server specific assets such as config files. Closes #24203
1 parent 78ce78c commit c29df69

File tree

9 files changed

+528
-66
lines changed

9 files changed

+528
-66
lines changed
 

‎goldens/public-api/angular_devkit/build_angular/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ export interface ProtractorBuilderOptions {
246246

247247
// @public (undocumented)
248248
export interface ServerBuilderOptions {
249+
assets?: AssetPattern_3[];
249250
deleteOutputPath?: boolean;
250251
// @deprecated
251252
deployUrl?: string;

‎packages/angular_devkit/build_angular/src/builders/browser/index.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -111,20 +111,6 @@ async function initialize(
111111
getStylesConfig(wco),
112112
]);
113113

114-
// Validate asset option values if processed directly
115-
if (options.assets?.length && !adjustedOptions.assets?.length) {
116-
normalizeAssetPatterns(
117-
options.assets,
118-
context.workspaceRoot,
119-
projectRoot,
120-
projectSourceRoot,
121-
).forEach(({ output }) => {
122-
if (output.startsWith('..')) {
123-
throw new Error('An asset cannot be written to a location outside of the output path.');
124-
}
125-
});
126-
}
127-
128114
let transformedConfig;
129115
if (webpackConfigurationTransform) {
130116
transformedConfig = await webpackConfigurationTransform(config);

‎packages/angular_devkit/build_angular/src/builders/browser/tests/options/assets_spec.ts

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -107,22 +107,17 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
107107
harness.expectFile('dist/test.svg').toNotExist();
108108
});
109109

110-
it('throws exception if asset path is not within project source root', async () => {
110+
it('fail if asset path is not within project source root', async () => {
111111
await harness.writeFile('test.svg', '<svg></svg>');
112112

113113
harness.useTarget('build', {
114114
...BASE_OPTIONS,
115115
assets: ['test.svg'],
116116
});
117117

118-
const { result, error } = await harness.executeOnce({ outputLogsOnException: false });
118+
const { result } = await harness.executeOnce();
119119

120-
expect(result).toBeUndefined();
121-
expect(error).toEqual(
122-
jasmine.objectContaining({
123-
message: jasmine.stringMatching('path must start with the project source root'),
124-
}),
125-
);
120+
expect(result?.error).toMatch('path must start with the project source root');
126121

127122
harness.expectFile('dist/test.svg').toNotExist();
128123
});
@@ -364,23 +359,18 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
364359
harness.expectFile('dist/subdirectory/test.svg').content.toBe('<svg></svg>');
365360
});
366361

367-
it('throws exception if output option is not within project output path', async () => {
362+
it('fails if output option is not within project output path', async () => {
368363
await harness.writeFile('test.svg', '<svg></svg>');
369364

370365
harness.useTarget('build', {
371366
...BASE_OPTIONS,
372367
assets: [{ glob: 'test.svg', input: 'src', output: '..' }],
373368
});
374369

375-
const { result, error } = await harness.executeOnce({ outputLogsOnException: false });
370+
const { result } = await harness.executeOnce();
376371

377-
expect(result).toBeUndefined();
378-
expect(error).toEqual(
379-
jasmine.objectContaining({
380-
message: jasmine.stringMatching(
381-
'An asset cannot be written to a location outside of the output path',
382-
),
383-
}),
372+
expect(result?.error).toMatch(
373+
'An asset cannot be written to a location outside of the output path',
384374
);
385375

386376
harness.expectFile('dist/test.svg').toNotExist();

‎packages/angular_devkit/build_angular/src/builders/server/index.ts

Lines changed: 78 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,22 @@ import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/ar
1010
import { runWebpack } from '@angular-devkit/build-webpack';
1111
import * as path from 'path';
1212
import { Observable, from } from 'rxjs';
13-
import { concatMap, map } from 'rxjs/operators';
13+
import { concatMap } from 'rxjs/operators';
1414
import webpack, { Configuration } from 'webpack';
1515
import { ExecutionTransformer } from '../../transforms';
16-
import { NormalizedBrowserBuilderSchema, deleteOutputDir } from '../../utils';
16+
import {
17+
NormalizedBrowserBuilderSchema,
18+
deleteOutputDir,
19+
normalizeAssetPatterns,
20+
} from '../../utils';
21+
import { colors } from '../../utils/color';
22+
import { copyAssets } from '../../utils/copy-assets';
23+
import { assertIsError } from '../../utils/error';
1724
import { i18nInlineEmittedFiles } from '../../utils/i18n-inlining';
1825
import { I18nOptions } from '../../utils/i18n-options';
1926
import { ensureOutputPaths } from '../../utils/output-paths';
2027
import { purgeStaleBuildCache } from '../../utils/purge-cache';
28+
import { Spinner } from '../../utils/spinner';
2129
import { assertCompatibleAngularVersion } from '../../utils/version';
2230
import {
2331
BrowserWebpackConfigOptions,
@@ -69,7 +77,7 @@ export function execute(
6977
let outputPaths: undefined | Map<string, string>;
7078

7179
return from(initialize(options, context, transforms.webpackConfiguration)).pipe(
72-
concatMap(({ config, i18n }) => {
80+
concatMap(({ config, i18n, projectRoot, projectSourceRoot }) => {
7381
return runWebpack(config, context, {
7482
webpackFactory: require('webpack') as typeof webpack,
7583
logging: (stats, config) => {
@@ -84,11 +92,43 @@ export function execute(
8492
throw new Error('Webpack stats build result is required.');
8593
}
8694

87-
let success = output.success;
88-
if (success && i18n.shouldInline) {
89-
outputPaths = ensureOutputPaths(baseOutputPath, i18n);
95+
if (!output.success) {
96+
return output;
97+
}
9098

91-
success = await i18nInlineEmittedFiles(
99+
const spinner = new Spinner();
100+
spinner.enabled = options.progress !== false;
101+
outputPaths = ensureOutputPaths(baseOutputPath, i18n);
102+
103+
// Copy assets
104+
if (!options.watch && options.assets?.length) {
105+
spinner.start('Copying assets...');
106+
try {
107+
await copyAssets(
108+
normalizeAssetPatterns(
109+
options.assets,
110+
context.workspaceRoot,
111+
projectRoot,
112+
projectSourceRoot,
113+
),
114+
Array.from(outputPaths.values()),
115+
context.workspaceRoot,
116+
);
117+
spinner.succeed('Copying assets complete.');
118+
} catch (err) {
119+
spinner.fail(colors.redBright('Copying of assets failed.'));
120+
assertIsError(err);
121+
122+
return {
123+
...output,
124+
success: false,
125+
error: 'Unable to copy assets: ' + err.message,
126+
};
127+
}
128+
}
129+
130+
if (i18n.shouldInline) {
131+
const success = await i18nInlineEmittedFiles(
92132
context,
93133
emittedFiles,
94134
i18n,
@@ -98,15 +138,21 @@ export function execute(
98138
outputPath,
99139
options.i18nMissingTranslation,
100140
);
141+
if (!success) {
142+
return {
143+
...output,
144+
success: false,
145+
};
146+
}
101147
}
102148

103149
webpackStatsLogger(context.logger, webpackStats, config);
104150

105-
return { ...output, success };
151+
return output;
106152
}),
107153
);
108154
}),
109-
map((output) => {
155+
concatMap(async (output) => {
110156
if (!output.success) {
111157
return output as ServerBuilderOutput;
112158
}
@@ -137,36 +183,42 @@ async function initialize(
137183
): Promise<{
138184
config: webpack.Configuration;
139185
i18n: I18nOptions;
186+
projectRoot: string;
187+
projectSourceRoot?: string;
140188
}> {
141189
// Purge old build disk cache.
142190
await purgeStaleBuildCache(context);
143191

144192
const browserslist = (await import('browserslist')).default;
145193
const originalOutputPath = options.outputPath;
146-
const { config, i18n } = await generateI18nBrowserWebpackConfigFromContext(
147-
{
148-
...options,
149-
buildOptimizer: false,
150-
aot: true,
151-
platform: 'server',
152-
} as NormalizedBrowserBuilderSchema,
153-
context,
154-
(wco) => {
155-
// We use the platform to determine the JavaScript syntax output.
156-
wco.buildOptions.supportedBrowsers ??= [];
157-
wco.buildOptions.supportedBrowsers.push(...browserslist('maintained node versions'));
158-
159-
return [getPlatformServerExportsConfig(wco), getCommonConfig(wco), getStylesConfig(wco)];
160-
},
161-
);
194+
// Assets are processed directly by the builder except when watching
195+
const adjustedOptions = options.watch ? options : { ...options, assets: [] };
196+
197+
const { config, projectRoot, projectSourceRoot, i18n } =
198+
await generateI18nBrowserWebpackConfigFromContext(
199+
{
200+
...adjustedOptions,
201+
buildOptimizer: false,
202+
aot: true,
203+
platform: 'server',
204+
} as NormalizedBrowserBuilderSchema,
205+
context,
206+
(wco) => {
207+
// We use the platform to determine the JavaScript syntax output.
208+
wco.buildOptions.supportedBrowsers ??= [];
209+
wco.buildOptions.supportedBrowsers.push(...browserslist('maintained node versions'));
210+
211+
return [getPlatformServerExportsConfig(wco), getCommonConfig(wco), getStylesConfig(wco)];
212+
},
213+
);
162214

163215
if (options.deleteOutputPath) {
164216
deleteOutputDir(context.workspaceRoot, originalOutputPath);
165217
}
166218

167219
const transformedConfig = (await webpackConfigurationTransform?.(config)) ?? config;
168220

169-
return { config: transformedConfig, i18n };
221+
return { config: transformedConfig, i18n, projectRoot, projectSourceRoot };
170222
}
171223

172224
/**

‎packages/angular_devkit/build_angular/src/builders/server/schema.json

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
"title": "Universal Target",
55
"type": "object",
66
"properties": {
7+
"assets": {
8+
"type": "array",
9+
"description": "List of static application assets.",
10+
"default": [],
11+
"items": {
12+
"$ref": "#/definitions/assetPattern"
13+
}
14+
},
715
"main": {
816
"type": "string",
917
"description": "The name of the main entry-point file."
@@ -212,6 +220,44 @@
212220
"additionalProperties": false,
213221
"required": ["outputPath", "main", "tsConfig"],
214222
"definitions": {
223+
"assetPattern": {
224+
"oneOf": [
225+
{
226+
"type": "object",
227+
"properties": {
228+
"followSymlinks": {
229+
"type": "boolean",
230+
"default": false,
231+
"description": "Allow glob patterns to follow symlink directories. This allows subdirectories of the symlink to be searched."
232+
},
233+
"glob": {
234+
"type": "string",
235+
"description": "The pattern to match."
236+
},
237+
"input": {
238+
"type": "string",
239+
"description": "The input directory path in which to apply 'glob'. Defaults to the project root."
240+
},
241+
"ignore": {
242+
"description": "An array of globs to ignore.",
243+
"type": "array",
244+
"items": {
245+
"type": "string"
246+
}
247+
},
248+
"output": {
249+
"type": "string",
250+
"description": "Absolute path within the output."
251+
}
252+
},
253+
"additionalProperties": false,
254+
"required": ["glob", "input", "output"]
255+
},
256+
{
257+
"type": "string"
258+
}
259+
]
260+
},
215261
"fileReplacement": {
216262
"oneOf": [
217263
{
Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { execute } from '../../index';
10+
import { BASE_OPTIONS, SERVER_BUILDER_INFO, describeBuilder } from '../setup';
11+
12+
describeBuilder(execute, SERVER_BUILDER_INFO, (harness) => {
13+
describe('Option: "assets"', () => {
14+
beforeEach(async () => {
15+
// Application code is not needed for asset tests
16+
await harness.writeFile('src/main.server.ts', '');
17+
});
18+
19+
it('supports an empty array value', async () => {
20+
harness.useTarget('server', {
21+
...BASE_OPTIONS,
22+
assets: [],
23+
});
24+
25+
const { result } = await harness.executeOnce();
26+
27+
expect(result?.success).toBeTrue();
28+
});
29+
30+
it('supports mixing shorthand and longhand syntax', async () => {
31+
await harness.writeFile('src/files/test.svg', '<svg></svg>');
32+
await harness.writeFile('src/files/another.file', 'asset file');
33+
await harness.writeFile('src/extra.file', 'extra file');
34+
35+
harness.useTarget('server', {
36+
...BASE_OPTIONS,
37+
assets: ['src/extra.file', { glob: '*', input: 'src/files', output: '.' }],
38+
});
39+
40+
const { result } = await harness.executeOnce();
41+
42+
expect(result?.success).toBeTrue();
43+
44+
harness.expectFile('dist/extra.file').content.toBe('extra file');
45+
harness.expectFile('dist/test.svg').content.toBe('<svg></svg>');
46+
harness.expectFile('dist/another.file').content.toBe('asset file');
47+
});
48+
49+
describe('shorthand syntax', () => {
50+
it('copies a single asset', async () => {
51+
await harness.writeFile('src/test.svg', '<svg></svg>');
52+
53+
harness.useTarget('server', {
54+
...BASE_OPTIONS,
55+
assets: ['src/test.svg'],
56+
});
57+
58+
const { result } = await harness.executeOnce();
59+
60+
expect(result?.success).toBeTrue();
61+
62+
harness.expectFile('dist/test.svg').content.toBe('<svg></svg>');
63+
});
64+
65+
it('copies multiple assets', async () => {
66+
await harness.writeFile('src/test.svg', '<svg></svg>');
67+
await harness.writeFile('src/another.file', 'asset file');
68+
69+
harness.useTarget('server', {
70+
...BASE_OPTIONS,
71+
assets: ['src/test.svg', 'src/another.file'],
72+
});
73+
74+
const { result } = await harness.executeOnce();
75+
76+
expect(result?.success).toBeTrue();
77+
78+
harness.expectFile('dist/test.svg').content.toBe('<svg></svg>');
79+
harness.expectFile('dist/another.file').content.toBe('asset file');
80+
});
81+
82+
it('copies an asset with directory and maintains directory in output', async () => {
83+
await harness.writeFile('src/subdirectory/test.svg', '<svg></svg>');
84+
85+
harness.useTarget('server', {
86+
...BASE_OPTIONS,
87+
assets: ['src/subdirectory/test.svg'],
88+
});
89+
90+
const { result } = await harness.executeOnce();
91+
92+
expect(result?.success).toBeTrue();
93+
94+
harness.expectFile('dist/subdirectory/test.svg').content.toBe('<svg></svg>');
95+
});
96+
97+
it('does not fail if asset does not exist', async () => {
98+
harness.useTarget('server', {
99+
...BASE_OPTIONS,
100+
assets: ['src/test.svg'],
101+
});
102+
103+
const { result } = await harness.executeOnce();
104+
105+
expect(result?.success).toBeTrue();
106+
107+
harness.expectFile('dist/test.svg').toNotExist();
108+
});
109+
110+
it('fails if output option is not within project output path', async () => {
111+
await harness.writeFile('test.svg', '<svg></svg>');
112+
113+
harness.useTarget('server', {
114+
...BASE_OPTIONS,
115+
assets: [{ glob: 'test.svg', input: 'src', output: '..' }],
116+
});
117+
118+
const { result } = await harness.executeOnce();
119+
120+
expect(result?.error).toMatch(
121+
'An asset cannot be written to a location outside of the output path',
122+
);
123+
124+
harness.expectFile('dist/test.svg').toNotExist();
125+
});
126+
});
127+
128+
describe('longhand syntax', () => {
129+
it('copies a single asset', async () => {
130+
await harness.writeFile('src/test.svg', '<svg></svg>');
131+
132+
harness.useTarget('server', {
133+
...BASE_OPTIONS,
134+
assets: [{ glob: 'test.svg', input: 'src', output: '.' }],
135+
});
136+
137+
const { result } = await harness.executeOnce();
138+
139+
expect(result?.success).toBeTrue();
140+
141+
harness.expectFile('dist/test.svg').content.toBe('<svg></svg>');
142+
});
143+
144+
it('copies multiple assets as separate entries', async () => {
145+
await harness.writeFile('src/test.svg', '<svg></svg>');
146+
await harness.writeFile('src/another.file', 'asset file');
147+
148+
harness.useTarget('server', {
149+
...BASE_OPTIONS,
150+
assets: [
151+
{ glob: 'test.svg', input: 'src', output: '.' },
152+
{ glob: 'another.file', input: 'src', output: '.' },
153+
],
154+
});
155+
156+
const { result } = await harness.executeOnce();
157+
158+
expect(result?.success).toBeTrue();
159+
160+
harness.expectFile('dist/test.svg').content.toBe('<svg></svg>');
161+
harness.expectFile('dist/another.file').content.toBe('asset file');
162+
});
163+
164+
it('copies multiple assets with a single entry glob pattern', async () => {
165+
await harness.writeFile('src/test.svg', '<svg></svg>');
166+
await harness.writeFile('src/another.file', 'asset file');
167+
168+
harness.useTarget('server', {
169+
...BASE_OPTIONS,
170+
assets: [{ glob: '{test.svg,another.file}', input: 'src', output: '.' }],
171+
});
172+
173+
const { result } = await harness.executeOnce();
174+
175+
expect(result?.success).toBeTrue();
176+
177+
harness.expectFile('dist/test.svg').content.toBe('<svg></svg>');
178+
harness.expectFile('dist/another.file').content.toBe('asset file');
179+
});
180+
181+
it('copies multiple assets with a wildcard glob pattern', async () => {
182+
await harness.writeFile('src/files/test.svg', '<svg></svg>');
183+
await harness.writeFile('src/files/another.file', 'asset file');
184+
185+
harness.useTarget('server', {
186+
...BASE_OPTIONS,
187+
assets: [{ glob: '*', input: 'src/files', output: '.' }],
188+
});
189+
190+
const { result } = await harness.executeOnce();
191+
192+
expect(result?.success).toBeTrue();
193+
194+
harness.expectFile('dist/test.svg').content.toBe('<svg></svg>');
195+
harness.expectFile('dist/another.file').content.toBe('asset file');
196+
});
197+
198+
it('copies multiple assets with a recursive wildcard glob pattern', async () => {
199+
await harness.writeFiles({
200+
'src/files/test.svg': '<svg></svg>',
201+
'src/files/another.file': 'asset file',
202+
'src/files/nested/extra.file': 'extra file',
203+
});
204+
205+
harness.useTarget('server', {
206+
...BASE_OPTIONS,
207+
assets: [{ glob: '**/*', input: 'src/files', output: '.' }],
208+
});
209+
210+
const { result } = await harness.executeOnce();
211+
212+
expect(result?.success).toBeTrue();
213+
214+
harness.expectFile('dist/test.svg').content.toBe('<svg></svg>');
215+
harness.expectFile('dist/another.file').content.toBe('asset file');
216+
harness.expectFile('dist/nested/extra.file').content.toBe('extra file');
217+
});
218+
219+
it('automatically ignores "." prefixed files when using wildcard glob pattern', async () => {
220+
await harness.writeFile('src/files/.gitkeep', '');
221+
222+
harness.useTarget('server', {
223+
...BASE_OPTIONS,
224+
assets: [{ glob: '*', input: 'src/files', output: '.' }],
225+
});
226+
227+
const { result } = await harness.executeOnce();
228+
229+
expect(result?.success).toBeTrue();
230+
231+
harness.expectFile('dist/.gitkeep').toNotExist();
232+
});
233+
234+
it('supports ignoring a specific file when using a glob pattern', async () => {
235+
await harness.writeFiles({
236+
'src/files/test.svg': '<svg></svg>',
237+
'src/files/another.file': 'asset file',
238+
'src/files/nested/extra.file': 'extra file',
239+
});
240+
241+
harness.useTarget('server', {
242+
...BASE_OPTIONS,
243+
assets: [{ glob: '**/*', input: 'src/files', output: '.', ignore: ['another.file'] }],
244+
});
245+
246+
const { result } = await harness.executeOnce();
247+
248+
expect(result?.success).toBeTrue();
249+
250+
harness.expectFile('dist/test.svg').content.toBe('<svg></svg>');
251+
harness.expectFile('dist/another.file').toNotExist();
252+
harness.expectFile('dist/nested/extra.file').content.toBe('extra file');
253+
});
254+
255+
it('supports ignoring with a glob pattern when using a glob pattern', async () => {
256+
await harness.writeFiles({
257+
'src/files/test.svg': '<svg></svg>',
258+
'src/files/another.file': 'asset file',
259+
'src/files/nested/extra.file': 'extra file',
260+
});
261+
262+
harness.useTarget('server', {
263+
...BASE_OPTIONS,
264+
assets: [{ glob: '**/*', input: 'src/files', output: '.', ignore: ['**/*.file'] }],
265+
});
266+
267+
const { result } = await harness.executeOnce();
268+
269+
expect(result?.success).toBeTrue();
270+
271+
harness.expectFile('dist/test.svg').content.toBe('<svg></svg>');
272+
harness.expectFile('dist/another.file').toNotExist();
273+
harness.expectFile('dist/nested/extra.file').toNotExist();
274+
});
275+
276+
it('copies an asset with directory and maintains directory in output', async () => {
277+
await harness.writeFile('src/subdirectory/test.svg', '<svg></svg>');
278+
279+
harness.useTarget('server', {
280+
...BASE_OPTIONS,
281+
assets: [{ glob: 'subdirectory/test.svg', input: 'src', output: '.' }],
282+
});
283+
284+
const { result } = await harness.executeOnce();
285+
286+
expect(result?.success).toBeTrue();
287+
288+
harness.expectFile('dist/subdirectory/test.svg').content.toBe('<svg></svg>');
289+
});
290+
291+
it('does not fail if asset does not exist', async () => {
292+
harness.useTarget('server', {
293+
...BASE_OPTIONS,
294+
assets: [{ glob: 'test.svg', input: 'src', output: '.' }],
295+
});
296+
297+
const { result } = await harness.executeOnce();
298+
299+
expect(result?.success).toBeTrue();
300+
301+
harness.expectFile('dist/test.svg').toNotExist();
302+
});
303+
304+
it('uses project output path when output option is empty string', async () => {
305+
await harness.writeFile('src/test.svg', '<svg></svg>');
306+
307+
harness.useTarget('server', {
308+
...BASE_OPTIONS,
309+
assets: [{ glob: 'test.svg', input: 'src', output: '' }],
310+
});
311+
312+
const { result } = await harness.executeOnce();
313+
314+
expect(result?.success).toBeTrue();
315+
316+
harness.expectFile('dist/test.svg').content.toBe('<svg></svg>');
317+
});
318+
319+
it('uses project output path when output option is "."', async () => {
320+
await harness.writeFile('src/test.svg', '<svg></svg>');
321+
322+
harness.useTarget('server', {
323+
...BASE_OPTIONS,
324+
assets: [{ glob: 'test.svg', input: 'src', output: '.' }],
325+
});
326+
327+
const { result } = await harness.executeOnce();
328+
329+
expect(result?.success).toBeTrue();
330+
331+
harness.expectFile('dist/test.svg').content.toBe('<svg></svg>');
332+
});
333+
334+
it('uses project output path when output option is "/"', async () => {
335+
await harness.writeFile('src/test.svg', '<svg></svg>');
336+
337+
harness.useTarget('server', {
338+
...BASE_OPTIONS,
339+
assets: [{ glob: 'test.svg', input: 'src', output: '/' }],
340+
});
341+
342+
const { result } = await harness.executeOnce();
343+
344+
expect(result?.success).toBeTrue();
345+
346+
harness.expectFile('dist/test.svg').content.toBe('<svg></svg>');
347+
});
348+
349+
it('creates a project output sub-path when output option path does not exist', async () => {
350+
await harness.writeFile('src/test.svg', '<svg></svg>');
351+
352+
harness.useTarget('server', {
353+
...BASE_OPTIONS,
354+
assets: [{ glob: 'test.svg', input: 'src', output: 'subdirectory' }],
355+
});
356+
357+
const { result } = await harness.executeOnce();
358+
359+
expect(result?.success).toBeTrue();
360+
361+
harness.expectFile('dist/subdirectory/test.svg').content.toBe('<svg></svg>');
362+
});
363+
364+
it('fails if output option is not within project output path', async () => {
365+
await harness.writeFile('test.svg', '<svg></svg>');
366+
367+
harness.useTarget('server', {
368+
...BASE_OPTIONS,
369+
assets: [{ glob: 'test.svg', input: 'src', output: '..' }],
370+
});
371+
372+
const { result } = await harness.executeOnce();
373+
374+
expect(result?.error).toMatch(
375+
'An asset cannot be written to a location outside of the output path',
376+
);
377+
378+
harness.expectFile('dist/test.svg').toNotExist();
379+
});
380+
});
381+
});
382+
});

‎packages/angular_devkit/build_angular/src/builders/server/tests/options/resources-output-path_spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ describeBuilder(execute, SERVER_BUILDER_INFO, (harness) => {
3535

3636
harness
3737
.expectFile('dist/main.js')
38-
.content.toContain(`url(/assets/component-img-absolute.png)`);
38+
.content.toContain(`url('/assets/component-img-absolute.png')`);
3939
harness
4040
.expectFile('dist/main.js')
41-
.content.toContain(`url(out-assets/component-img-relative.png)`);
41+
.content.toContain(`url('out-assets/component-img-relative.png')`);
4242

4343
// Assets are not emitted during a server builds.
4444
harness.expectFile('dist/out-assets/component-img-relative.png').toNotExist();
@@ -54,8 +54,8 @@ describeBuilder(execute, SERVER_BUILDER_INFO, (harness) => {
5454

5555
harness
5656
.expectFile('dist/main.js')
57-
.content.toContain(`url(/assets/component-img-absolute.png)`);
58-
harness.expectFile('dist/main.js').content.toContain(`url(component-img-relative.png)`);
57+
.content.toContain(`url('/assets/component-img-absolute.png')`);
58+
harness.expectFile('dist/main.js').content.toContain(`url('component-img-relative.png')`);
5959

6060
// Assets are not emitted during a server builds.
6161
harness.expectFile('dist/component-img-relative.png').toNotExist();

‎packages/angular_devkit/build_angular/src/builders/server/tests/setup.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,7 @@ export const BASE_OPTIONS = Object.freeze<Schema>({
2525
progress: false,
2626
watch: false,
2727
outputPath: 'dist',
28+
29+
// Disable optimizations
30+
optimization: false,
2831
});

‎packages/angular_devkit/build_angular/src/utils/normalize-asset-patterns.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,13 @@ export function normalizeAssetPatterns(
6666
// Output directory for both is the relative path from source root to input.
6767
const output = path.relative(resolvedSourceRoot, path.resolve(workspaceRoot, input));
6868

69-
// Return the asset pattern in object format.
70-
return { glob, input, output };
71-
} else {
72-
// It's already an AssetPatternObject, no need to convert.
73-
return assetPattern;
69+
assetPattern = { glob, input, output };
7470
}
71+
72+
if (assetPattern.output.startsWith('..')) {
73+
throw new Error('An asset cannot be written to a location outside of the output path.');
74+
}
75+
76+
return assetPattern;
7577
});
7678
}

0 commit comments

Comments
 (0)
Please sign in to comment.