Skip to content

Commit 6822756

Browse files
alan-agius4vikerman
authored andcommitted
feat(@angular-devkit/build-angular): add crossorigin options
This options allows to define the crossorigin attribute setting of elements that provide CORS support Closes #14743
1 parent 860e12b commit 6822756

File tree

8 files changed

+165
-49
lines changed

8 files changed

+165
-49
lines changed

packages/angular/cli/lib/config/schema.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,16 @@
890890
"webWorkerTsConfig": {
891891
"type": "string",
892892
"description": "TypeScript configuration for Web Worker modules."
893+
},
894+
"crossOrigin": {
895+
"type": "string",
896+
"description": "Define the crossorigin attribute setting of elements that provide CORS support.",
897+
"default": "none",
898+
"enum": [
899+
"none",
900+
"anonymous",
901+
"use-credentials"
902+
]
893903
}
894904
},
895905
"additionalProperties": false,

packages/angular_devkit/build_angular/src/angular-cli-files/plugins/index-html-webpack-plugin.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
import * as path from 'path';
99
import { Compiler, compilation } from 'webpack';
1010
import { RawSource } from 'webpack-sources';
11-
import { FileInfo, augmentIndexHtml } from '../utilities/index-file/augment-index-html';
11+
import {
12+
CrossOriginValue,
13+
FileInfo,
14+
augmentIndexHtml,
15+
} from '../utilities/index-file/augment-index-html';
1216
import { IndexHtmlTransform } from '../utilities/index-file/write-index-html';
1317
import { stripBom } from '../utilities/strip-bom';
1418

@@ -22,6 +26,7 @@ export interface IndexHtmlWebpackPluginOptions {
2226
noModuleEntrypoints: string[];
2327
moduleEntrypoints: string[];
2428
postTransform?: IndexHtmlTransform;
29+
crossOrigin?: CrossOriginValue;
2530
}
2631

2732
function readFile(filename: string, compilation: compilation.Compilation): Promise<string> {
@@ -91,6 +96,7 @@ export class IndexHtmlWebpackPlugin {
9196
baseHref: this._options.baseHref,
9297
deployUrl: this._options.deployUrl,
9398
sri: this._options.sri,
99+
crossOrigin: this._options.crossOrigin,
94100
files,
95101
noModuleFiles,
96102
loadOutputFile,

packages/angular_devkit/build_angular/src/angular-cli-files/utilities/index-file/augment-index-html.ts

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import { RawSource, ReplaceSource } from 'webpack-sources';
1111

1212
const parse5 = require('parse5');
1313

14-
1514
export type LoadOutputFileFunctionType = (file: string) => Promise<string>;
1615

16+
export type CrossOriginValue = 'none' | 'anonymous' | 'use-credentials';
17+
1718
export interface AugmentIndexHtmlOptions {
1819
/* Input file name (e. g. index.html) */
1920
input: string;
@@ -22,6 +23,8 @@ export interface AugmentIndexHtmlOptions {
2223
baseHref?: string;
2324
deployUrl?: string;
2425
sri: boolean;
26+
/** crossorigin attribute setting of elements that provide CORS support */
27+
crossOrigin?: CrossOriginValue;
2528
/*
2629
* Files emitted by the build.
2730
* Js files will be added without 'nomodule' nor 'module'.
@@ -54,13 +57,12 @@ export interface FileInfo {
5457
* bundles for differential serving.
5558
*/
5659
export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise<string> {
57-
const {
58-
loadOutputFile,
59-
files,
60-
noModuleFiles = [],
61-
moduleFiles = [],
62-
entrypoints,
63-
} = params;
60+
const { loadOutputFile, files, noModuleFiles = [], moduleFiles = [], entrypoints } = params;
61+
62+
let { crossOrigin = 'none' } = params;
63+
if (params.sri && crossOrigin === 'none') {
64+
crossOrigin = 'anonymous';
65+
}
6466

6567
const stylesheets = new Set<string>();
6668
const scripts = new Set<string>();
@@ -69,7 +71,9 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
6971
const mergedFiles = [...moduleFiles, ...noModuleFiles, ...files];
7072
for (const entrypoint of entrypoints) {
7173
for (const { extension, file, name } of mergedFiles) {
72-
if (name !== entrypoint) { continue; }
74+
if (name !== entrypoint) {
75+
continue;
76+
}
7377

7478
switch (extension) {
7579
case '.js':
@@ -127,10 +131,14 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
127131

128132
let scriptElements = '';
129133
for (const script of scripts) {
130-
const attrs: { name: string, value: string | null }[] = [
134+
const attrs: { name: string; value: string | null }[] = [
131135
{ name: 'src', value: (params.deployUrl || '') + script },
132136
];
133137

138+
if (crossOrigin !== 'none') {
139+
attrs.push({ name: 'crossorigin', value: crossOrigin });
140+
}
141+
134142
// We want to include nomodule or module when a file is not common amongs all
135143
// such as runtime.js
136144
const scriptPredictor = ({ file }: FileInfo): boolean => file === script;
@@ -154,15 +162,12 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
154162
}
155163

156164
const attributes = attrs
157-
.map(attr => attr.value === null ? attr.name : `${attr.name}="${attr.value}"`)
165+
.map(attr => (attr.value === null ? attr.name : `${attr.name}="${attr.value}"`))
158166
.join(' ');
159167
scriptElements += `<script ${attributes}></script>`;
160168
}
161169

162-
indexSource.insert(
163-
scriptInsertionPoint,
164-
scriptElements,
165-
);
170+
indexSource.insert(scriptInsertionPoint, scriptElements);
166171

167172
// Adjust base href if specified
168173
if (typeof params.baseHref == 'string') {
@@ -176,13 +181,9 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
176181
const baseFragment = treeAdapter.createDocumentFragment();
177182

178183
if (!baseElement) {
179-
baseElement = treeAdapter.createElement(
180-
'base',
181-
undefined,
182-
[
183-
{ name: 'href', value: params.baseHref },
184-
],
185-
);
184+
baseElement = treeAdapter.createElement('base', undefined, [
185+
{ name: 'href', value: params.baseHref },
186+
]);
186187

187188
treeAdapter.appendChild(baseFragment, baseElement);
188189
indexSource.insert(
@@ -218,6 +219,10 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
218219
{ name: 'href', value: (params.deployUrl || '') + stylesheet },
219220
];
220221

222+
if (crossOrigin !== 'none') {
223+
attrs.push({ name: 'crossorigin', value: crossOrigin });
224+
}
225+
221226
if (params.sri) {
222227
const content = await loadOutputFile(stylesheet);
223228
attrs.push(..._generateSriAttributes(content));
@@ -227,10 +232,7 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
227232
treeAdapter.appendChild(styleElements, element);
228233
}
229234

230-
indexSource.insert(
231-
styleInsertionPoint,
232-
parse5.serialize(styleElements, { treeAdapter }),
233-
);
235+
indexSource.insert(styleInsertionPoint, parse5.serialize(styleElements, { treeAdapter }));
234236

235237
return indexSource.source();
236238
}
@@ -241,8 +243,5 @@ function _generateSriAttributes(content: string) {
241243
.update(content, 'utf8')
242244
.digest('base64');
243245

244-
return [
245-
{ name: 'integrity', value: `${algo}-${hash}` },
246-
{ name: 'crossorigin', value: 'anonymous' },
247-
];
246+
return [{ name: 'integrity', value: `${algo}-${hash}` }];
248247
}

packages/angular_devkit/build_angular/src/angular-cli-files/utilities/index-file/write-index-html.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { map, switchMap } from 'rxjs/operators';
1313
import { ExtraEntryPoint } from '../../../browser/schema';
1414
import { generateEntryPoints } from '../package-chunk-sort';
1515
import { stripBom } from '../strip-bom';
16-
import { FileInfo, augmentIndexHtml } from './augment-index-html';
16+
import { CrossOriginValue, FileInfo, augmentIndexHtml } from './augment-index-html';
1717

1818
type ExtensionFilter = '.js' | '.css';
1919

@@ -30,6 +30,7 @@ export interface WriteIndexHtmlOptions {
3030
scripts?: ExtraEntryPoint[];
3131
styles?: ExtraEntryPoint[];
3232
postTransform?: IndexHtmlTransform;
33+
crossOrigin?: CrossOriginValue;
3334
}
3435

3536
export type IndexHtmlTransform = (content: string) => Promise<string>;
@@ -47,34 +48,34 @@ export function writeIndexHtml({
4748
scripts = [],
4849
styles = [],
4950
postTransform,
51+
crossOrigin,
5052
}: WriteIndexHtmlOptions): Observable<void> {
51-
52-
return host.read(indexPath)
53-
.pipe(
54-
map(content => stripBom(virtualFs.fileBufferToString(content))),
55-
switchMap(content => augmentIndexHtml({
53+
return host.read(indexPath).pipe(
54+
map(content => stripBom(virtualFs.fileBufferToString(content))),
55+
switchMap(content =>
56+
augmentIndexHtml({
5657
input: getSystemPath(outputPath),
5758
inputContent: content,
5859
baseHref,
5960
deployUrl,
61+
crossOrigin,
6062
sri,
6163
entrypoints: generateEntryPoints({ scripts, styles }),
6264
files: filterAndMapBuildFiles(files, ['.js', '.css']),
6365
noModuleFiles: filterAndMapBuildFiles(noModuleFiles, '.js'),
6466
moduleFiles: filterAndMapBuildFiles(moduleFiles, '.js'),
6567
loadOutputFile: async filePath => {
66-
return host.read(join(outputPath, filePath))
67-
.pipe(
68-
map(data => virtualFs.fileBufferToString(data)),
69-
)
68+
return host
69+
.read(join(outputPath, filePath))
70+
.pipe(map(data => virtualFs.fileBufferToString(data)))
7071
.toPromise();
7172
},
7273
}),
73-
),
74-
switchMap(content => postTransform ? postTransform(content) : of(content)),
75-
map(content => virtualFs.stringToFileBuffer(content)),
76-
switchMap(content => host.write(join(outputPath, basename(indexPath)), content)),
77-
);
74+
),
75+
switchMap(content => (postTransform ? postTransform(content) : of(content))),
76+
map(content => virtualFs.stringToFileBuffer(content)),
77+
switchMap(content => host.write(join(outputPath, basename(indexPath)), content)),
78+
);
7879
}
7980

8081
function filterAndMapBuildFiles(

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,8 @@ function getAnalyticsConfig(
115115
let category = 'build';
116116
if (context.builder) {
117117
// We already vetted that this is a "safe" package, otherwise the analytics would be noop.
118-
category = context.builder.builderName.split(':')[1]
119-
|| context.builder.builderName
120-
|| 'build';
118+
category =
119+
context.builder.builderName.split(':')[1] || context.builder.builderName || 'build';
121120
}
122121

123122
// The category is the builder name if it's an angular builder.
@@ -264,6 +263,7 @@ export function buildWebpackBrowser(
264263
scripts: options.scripts,
265264
styles: options.styles,
266265
postTransform: transforms.indexHtml,
266+
crossOrigin: options.crossOrigin,
267267
}).pipe(
268268
map(() => ({ success: true })),
269269
catchError(error => of({ success: false, error: mapErrorToMessage(error) })),

packages/angular_devkit/build_angular/src/browser/schema.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,16 @@
319319
"webWorkerTsConfig": {
320320
"type": "string",
321321
"description": "TypeScript configuration for Web Worker modules."
322+
},
323+
"crossOrigin": {
324+
"type": "string",
325+
"description": "Define the crossorigin attribute setting of elements that provide CORS support.",
326+
"default": "none",
327+
"enum": [
328+
"none",
329+
"anonymous",
330+
"use-credentials"
331+
]
322332
}
323333
},
324334
"additionalProperties": false,

packages/angular_devkit/build_angular/src/dev-server/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ export function serveWebpackBrowser(
223223
sri: browserOptions.subresourceIntegrity,
224224
noModuleEntrypoints: ['polyfills-es5'],
225225
postTransform: transforms.indexHtml,
226+
crossOrigin: browserOptions.crossOrigin,
226227
}),
227228
);
228229
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. 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 { Architect } from '@angular-devkit/architect';
10+
import { join, normalize, virtualFs } from '@angular-devkit/core';
11+
import { BrowserBuilderOutput } from '../../src/browser/index';
12+
import { CrossOrigin } from '../../src/browser/schema';
13+
import { createArchitect, host } from '../utils';
14+
15+
describe('Browser Builder crossOrigin', () => {
16+
const targetSpec = { project: 'app', target: 'build' };
17+
let architect: Architect;
18+
19+
beforeEach(async () => {
20+
await host.initialize().toPromise();
21+
architect = (await createArchitect(host.root())).architect;
22+
23+
host.writeMultipleFiles({
24+
'src/index.html': Buffer.from(
25+
'\ufeff<html><head><base href="/"></head><body><app-root></app-root></body></html>',
26+
'utf8',
27+
),
28+
});
29+
});
30+
31+
afterEach(async () => host.restore().toPromise());
32+
33+
it('works with use-credentials', async () => {
34+
const overrides = { crossOrigin: CrossOrigin.UseCredentials };
35+
const run = await architect.scheduleTarget(targetSpec, overrides);
36+
const output = (await run.result) as BrowserBuilderOutput;
37+
expect(output.success).toBe(true);
38+
const fileName = join(normalize(output.outputPath), 'index.html');
39+
const content = virtualFs.fileBufferToString(await host.read(normalize(fileName)).toPromise());
40+
expect(content).toBe(
41+
`<html><head><base href="/"></head>` +
42+
`<body><app-root></app-root>` +
43+
`<script src="runtime.js" crossorigin="use-credentials"></script>` +
44+
`<script src="polyfills.js" crossorigin="use-credentials"></script>` +
45+
`<script src="styles.js" crossorigin="use-credentials"></script>` +
46+
`<script src="vendor.js" crossorigin="use-credentials"></script>` +
47+
`<script src="main.js" crossorigin="use-credentials"></script></body></html>`,
48+
);
49+
await run.stop();
50+
});
51+
52+
it('works with anonymous', async () => {
53+
const overrides = { crossOrigin: CrossOrigin.Anonymous };
54+
const run = await architect.scheduleTarget(targetSpec, overrides);
55+
const output = (await run.result) as BrowserBuilderOutput;
56+
expect(output.success).toBe(true);
57+
const fileName = join(normalize(output.outputPath), 'index.html');
58+
const content = virtualFs.fileBufferToString(await host.read(normalize(fileName)).toPromise());
59+
expect(content).toBe(
60+
`<html><head><base href="/"></head>` +
61+
`<body><app-root></app-root>` +
62+
`<script src="runtime.js" crossorigin="anonymous"></script>` +
63+
`<script src="polyfills.js" crossorigin="anonymous"></script>` +
64+
`<script src="styles.js" crossorigin="anonymous"></script>` +
65+
`<script src="vendor.js" crossorigin="anonymous"></script>` +
66+
`<script src="main.js" crossorigin="anonymous"></script></body></html>`,
67+
);
68+
await run.stop();
69+
});
70+
71+
it('works with none', async () => {
72+
const overrides = { crossOrigin: CrossOrigin.None };
73+
const run = await architect.scheduleTarget(targetSpec, overrides);
74+
const output = (await run.result) as BrowserBuilderOutput;
75+
expect(output.success).toBe(true);
76+
const fileName = join(normalize(output.outputPath), 'index.html');
77+
const content = virtualFs.fileBufferToString(await host.read(normalize(fileName)).toPromise());
78+
expect(content).toBe(
79+
`<html><head><base href="/"></head>` +
80+
`<body><app-root></app-root>` +
81+
`<script src="runtime.js"></script>` +
82+
`<script src="polyfills.js"></script>` +
83+
`<script src="styles.js"></script>` +
84+
`<script src="vendor.js"></script>` +
85+
`<script src="main.js"></script></body></html>`,
86+
);
87+
await run.stop();
88+
});
89+
});

0 commit comments

Comments
 (0)