Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/angular/cli/lib/config/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,16 @@
"webWorkerTsConfig": {
"type": "string",
"description": "TypeScript configuration for Web Worker modules."
},
"crossOrigin": {
"type": "string",
"description": "Define the crossorigin attribute setting of elements that provide CORS support.",
"default": "none",
"enum": [
"none",
"anonymous",
"use-credentials"
]
}
},
"additionalProperties": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
import * as path from 'path';
import { Compiler, compilation } from 'webpack';
import { RawSource } from 'webpack-sources';
import { FileInfo, augmentIndexHtml } from '../utilities/index-file/augment-index-html';
import {
CrossOriginValue,
FileInfo,
augmentIndexHtml,
} from '../utilities/index-file/augment-index-html';
import { IndexHtmlTransform } from '../utilities/index-file/write-index-html';
import { stripBom } from '../utilities/strip-bom';

Expand All @@ -22,6 +26,7 @@ export interface IndexHtmlWebpackPluginOptions {
noModuleEntrypoints: string[];
moduleEntrypoints: string[];
postTransform?: IndexHtmlTransform;
crossOrigin?: CrossOriginValue;
}

function readFile(filename: string, compilation: compilation.Compilation): Promise<string> {
Expand Down Expand Up @@ -91,6 +96,7 @@ export class IndexHtmlWebpackPlugin {
baseHref: this._options.baseHref,
deployUrl: this._options.deployUrl,
sri: this._options.sri,
crossOrigin: this._options.crossOrigin,
files,
noModuleFiles,
loadOutputFile,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import { RawSource, ReplaceSource } from 'webpack-sources';

const parse5 = require('parse5');


export type LoadOutputFileFunctionType = (file: string) => Promise<string>;

export type CrossOriginValue = 'none' | 'anonymous' | 'use-credentials';

export interface AugmentIndexHtmlOptions {
/* Input file name (e. g. index.html) */
input: string;
Expand All @@ -22,6 +23,8 @@ export interface AugmentIndexHtmlOptions {
baseHref?: string;
deployUrl?: string;
sri: boolean;
/** crossorigin attribute setting of elements that provide CORS support */
crossOrigin?: CrossOriginValue;
/*
* Files emitted by the build.
* Js files will be added without 'nomodule' nor 'module'.
Expand Down Expand Up @@ -54,13 +57,12 @@ export interface FileInfo {
* bundles for differential serving.
*/
export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise<string> {
const {
loadOutputFile,
files,
noModuleFiles = [],
moduleFiles = [],
entrypoints,
} = params;
const { loadOutputFile, files, noModuleFiles = [], moduleFiles = [], entrypoints } = params;

let { crossOrigin = 'none' } = params;
if (params.sri && crossOrigin === 'none') {
crossOrigin = 'anonymous';
}

const stylesheets = new Set<string>();
const scripts = new Set<string>();
Expand All @@ -69,7 +71,9 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
const mergedFiles = [...moduleFiles, ...noModuleFiles, ...files];
for (const entrypoint of entrypoints) {
for (const { extension, file, name } of mergedFiles) {
if (name !== entrypoint) { continue; }
if (name !== entrypoint) {
continue;
}

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

let scriptElements = '';
for (const script of scripts) {
const attrs: { name: string, value: string | null }[] = [
const attrs: { name: string; value: string | null }[] = [
{ name: 'src', value: (params.deployUrl || '') + script },
];

if (crossOrigin !== 'none') {
attrs.push({ name: 'crossorigin', value: crossOrigin });
}

// We want to include nomodule or module when a file is not common amongs all
// such as runtime.js
const scriptPredictor = ({ file }: FileInfo): boolean => file === script;
Expand All @@ -154,15 +162,12 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
}

const attributes = attrs
.map(attr => attr.value === null ? attr.name : `${attr.name}="${attr.value}"`)
.map(attr => (attr.value === null ? attr.name : `${attr.name}="${attr.value}"`))
.join(' ');
scriptElements += `<script ${attributes}></script>`;
}

indexSource.insert(
scriptInsertionPoint,
scriptElements,
);
indexSource.insert(scriptInsertionPoint, scriptElements);

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

if (!baseElement) {
baseElement = treeAdapter.createElement(
'base',
undefined,
[
{ name: 'href', value: params.baseHref },
],
);
baseElement = treeAdapter.createElement('base', undefined, [
{ name: 'href', value: params.baseHref },
]);

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

if (crossOrigin !== 'none') {
attrs.push({ name: 'crossorigin', value: crossOrigin });
}

if (params.sri) {
const content = await loadOutputFile(stylesheet);
attrs.push(..._generateSriAttributes(content));
Expand All @@ -227,10 +232,7 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
treeAdapter.appendChild(styleElements, element);
}

indexSource.insert(
styleInsertionPoint,
parse5.serialize(styleElements, { treeAdapter }),
);
indexSource.insert(styleInsertionPoint, parse5.serialize(styleElements, { treeAdapter }));

return indexSource.source();
}
Expand All @@ -241,8 +243,5 @@ function _generateSriAttributes(content: string) {
.update(content, 'utf8')
.digest('base64');

return [
{ name: 'integrity', value: `${algo}-${hash}` },
{ name: 'crossorigin', value: 'anonymous' },
];
return [{ name: 'integrity', value: `${algo}-${hash}` }];
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { map, switchMap } from 'rxjs/operators';
import { ExtraEntryPoint } from '../../../browser/schema';
import { generateEntryPoints } from '../package-chunk-sort';
import { stripBom } from '../strip-bom';
import { FileInfo, augmentIndexHtml } from './augment-index-html';
import { CrossOriginValue, FileInfo, augmentIndexHtml } from './augment-index-html';

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

Expand All @@ -30,6 +30,7 @@ export interface WriteIndexHtmlOptions {
scripts?: ExtraEntryPoint[];
styles?: ExtraEntryPoint[];
postTransform?: IndexHtmlTransform;
crossOrigin?: CrossOriginValue;
}

export type IndexHtmlTransform = (content: string) => Promise<string>;
Expand All @@ -47,34 +48,34 @@ export function writeIndexHtml({
scripts = [],
styles = [],
postTransform,
crossOrigin,
}: WriteIndexHtmlOptions): Observable<void> {

return host.read(indexPath)
.pipe(
map(content => stripBom(virtualFs.fileBufferToString(content))),
switchMap(content => augmentIndexHtml({
return host.read(indexPath).pipe(
map(content => stripBom(virtualFs.fileBufferToString(content))),
switchMap(content =>
augmentIndexHtml({
input: getSystemPath(outputPath),
inputContent: content,
baseHref,
deployUrl,
crossOrigin,
sri,
entrypoints: generateEntryPoints({ scripts, styles }),
files: filterAndMapBuildFiles(files, ['.js', '.css']),
noModuleFiles: filterAndMapBuildFiles(noModuleFiles, '.js'),
moduleFiles: filterAndMapBuildFiles(moduleFiles, '.js'),
loadOutputFile: async filePath => {
return host.read(join(outputPath, filePath))
.pipe(
map(data => virtualFs.fileBufferToString(data)),
)
return host
.read(join(outputPath, filePath))
.pipe(map(data => virtualFs.fileBufferToString(data)))
.toPromise();
},
}),
),
switchMap(content => postTransform ? postTransform(content) : of(content)),
map(content => virtualFs.stringToFileBuffer(content)),
switchMap(content => host.write(join(outputPath, basename(indexPath)), content)),
);
),
switchMap(content => (postTransform ? postTransform(content) : of(content))),
map(content => virtualFs.stringToFileBuffer(content)),
switchMap(content => host.write(join(outputPath, basename(indexPath)), content)),
);
}

function filterAndMapBuildFiles(
Expand Down
6 changes: 3 additions & 3 deletions packages/angular_devkit/build_angular/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,8 @@ function getAnalyticsConfig(
let category = 'build';
if (context.builder) {
// We already vetted that this is a "safe" package, otherwise the analytics would be noop.
category = context.builder.builderName.split(':')[1]
|| context.builder.builderName
|| 'build';
category =
context.builder.builderName.split(':')[1] || context.builder.builderName || 'build';
}

// The category is the builder name if it's an angular builder.
Expand Down Expand Up @@ -264,6 +263,7 @@ export function buildWebpackBrowser(
scripts: options.scripts,
styles: options.styles,
postTransform: transforms.indexHtml,
crossOrigin: options.crossOrigin,
}).pipe(
map(() => ({ success: true })),
catchError(error => of({ success: false, error: mapErrorToMessage(error) })),
Expand Down
10 changes: 10 additions & 0 deletions packages/angular_devkit/build_angular/src/browser/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,16 @@
"webWorkerTsConfig": {
"type": "string",
"description": "TypeScript configuration for Web Worker modules."
},
"crossOrigin": {
"type": "string",
"description": "Define the crossorigin attribute setting of elements that provide CORS support.",
"default": "none",
"enum": [
"none",
"anonymous",
"use-credentials"
]
}
},
"additionalProperties": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export function serveWebpackBrowser(
sri: browserOptions.subresourceIntegrity,
noModuleEntrypoints: ['polyfills-es5'],
postTransform: transforms.indexHtml,
crossOrigin: browserOptions.crossOrigin,
}),
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* @license
* Copyright Google Inc. 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 { Architect } from '@angular-devkit/architect';
import { join, normalize, virtualFs } from '@angular-devkit/core';
import { BrowserBuilderOutput } from '../../src/browser/index';
import { CrossOrigin } from '../../src/browser/schema';
import { createArchitect, host } from '../utils';

describe('Browser Builder crossOrigin', () => {
const targetSpec = { project: 'app', target: 'build' };
let architect: Architect;

beforeEach(async () => {
await host.initialize().toPromise();
architect = (await createArchitect(host.root())).architect;

host.writeMultipleFiles({
'src/index.html': Buffer.from(
'\ufeff<html><head><base href="/"></head><body><app-root></app-root></body></html>',
'utf8',
),
});
});

afterEach(async () => host.restore().toPromise());

it('works with use-credentials', async () => {
const overrides = { crossOrigin: CrossOrigin.UseCredentials };
const run = await architect.scheduleTarget(targetSpec, overrides);
const output = (await run.result) as BrowserBuilderOutput;
expect(output.success).toBe(true);
const fileName = join(normalize(output.outputPath), 'index.html');
const content = virtualFs.fileBufferToString(await host.read(normalize(fileName)).toPromise());
expect(content).toBe(
`<html><head><base href="/"></head>` +
`<body><app-root></app-root>` +
`<script src="runtime.js" crossorigin="use-credentials"></script>` +
`<script src="polyfills.js" crossorigin="use-credentials"></script>` +
`<script src="styles.js" crossorigin="use-credentials"></script>` +
`<script src="vendor.js" crossorigin="use-credentials"></script>` +
`<script src="main.js" crossorigin="use-credentials"></script></body></html>`,
);
await run.stop();
});

it('works with anonymous', async () => {
const overrides = { crossOrigin: CrossOrigin.Anonymous };
const run = await architect.scheduleTarget(targetSpec, overrides);
const output = (await run.result) as BrowserBuilderOutput;
expect(output.success).toBe(true);
const fileName = join(normalize(output.outputPath), 'index.html');
const content = virtualFs.fileBufferToString(await host.read(normalize(fileName)).toPromise());
expect(content).toBe(
`<html><head><base href="/"></head>` +
`<body><app-root></app-root>` +
`<script src="runtime.js" crossorigin="anonymous"></script>` +
`<script src="polyfills.js" crossorigin="anonymous"></script>` +
`<script src="styles.js" crossorigin="anonymous"></script>` +
`<script src="vendor.js" crossorigin="anonymous"></script>` +
`<script src="main.js" crossorigin="anonymous"></script></body></html>`,
);
await run.stop();
});

it('works with none', async () => {
const overrides = { crossOrigin: CrossOrigin.None };
const run = await architect.scheduleTarget(targetSpec, overrides);
const output = (await run.result) as BrowserBuilderOutput;
expect(output.success).toBe(true);
const fileName = join(normalize(output.outputPath), 'index.html');
const content = virtualFs.fileBufferToString(await host.read(normalize(fileName)).toPromise());
expect(content).toBe(
`<html><head><base href="/"></head>` +
`<body><app-root></app-root>` +
`<script src="runtime.js"></script>` +
`<script src="polyfills.js"></script>` +
`<script src="styles.js"></script>` +
`<script src="vendor.js"></script>` +
`<script src="main.js"></script></body></html>`,
);
await run.stop();
});
});