Skip to content
This repository has been archived by the owner on Apr 9, 2022. It is now read-only.

Commit

Permalink
fix(@angular-devkit/build-angular): perform surgical updates to index…
Browse files Browse the repository at this point in the history
  • Loading branch information
clydin authored and filipesilva committed May 16, 2018
1 parent 5c442a9 commit a820750
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import { createHash } from 'crypto';
import { Compiler, compilation } from 'webpack';
import { RawSource } from 'webpack-sources';
import { RawSource, ReplaceSource } from 'webpack-sources';

const parse5 = require('parse5');

Expand Down Expand Up @@ -95,71 +95,94 @@ export class IndexHtmlWebpackPlugin {

// Find the head and body elements
const treeAdapter = parse5.treeAdapters.default;
const document = parse5.parse(inputContent, { treeAdapter });
const document = parse5.parse(inputContent, { treeAdapter, locationInfo: true });
let headElement;
let bodyElement;
for (const topNode of document.childNodes) {
if (topNode.tagName === 'html') {
for (const htmlNode of topNode.childNodes) {
if (htmlNode.tagName === 'head') {
headElement = htmlNode;
for (const docChild of document.childNodes) {
if (docChild.tagName === 'html') {
for (const htmlChild of docChild.childNodes) {
if (htmlChild.tagName === 'head') {
headElement = htmlChild;
}
if (htmlNode.tagName === 'body') {
bodyElement = htmlNode;
if (htmlChild.tagName === 'body') {
bodyElement = htmlChild;
}
}
}
}

// Inject into the html

if (!headElement || !bodyElement) {
throw new Error('Missing head and/or body elements');
}

// Determine script insertion point
let scriptInsertionPoint;
if (bodyElement.__location && bodyElement.__location.endTag) {
scriptInsertionPoint = bodyElement.__location.endTag.startOffset;
} else {
// Less accurate fallback
// parse5 4.x does not provide locations if malformed html is present
scriptInsertionPoint = inputContent.indexOf('</body>');
}

let styleInsertionPoint;
if (headElement.__location && headElement.__location.endTag) {
styleInsertionPoint = headElement.__location.endTag.startOffset;
} else {
// Less accurate fallback
// parse5 4.x does not provide locations if malformed html is present
styleInsertionPoint = inputContent.indexOf('</head>');
}

// Inject into the html
const indexSource = new ReplaceSource(new RawSource(inputContent), this._options.input);

const scriptElements = treeAdapter.createDocumentFragment();
for (const script of scripts) {
const attrs = [
{ name: 'type', value: 'text/javascript' },
{ name: 'src', value: (this._options.deployUrl || '') + script },
];

if (this._options.sri) {
const algo = 'sha384';
const hash = createHash(algo)
.update(compilation.assets[script].source(), 'utf8')
.digest('base64');
attrs.push(
{ name: 'integrity', value: `${algo}-${hash}` },
{ name: 'crossorigin', value: 'anonymous' },
);
const content = compilation.assets[script].source();
attrs.push(...this._generateSriAttributes(content));
}

const element = treeAdapter.createElement(
'script',
undefined,
attrs,
);
treeAdapter.appendChild(bodyElement, element);
const element = treeAdapter.createElement('script', undefined, attrs);
treeAdapter.appendChild(scriptElements, element);
}

indexSource.insert(
scriptInsertionPoint,
parse5.serialize(scriptElements, { treeAdapter }),
);

// Adjust base href if specified
if (this._options.baseHref != undefined) {
if (typeof this._options.baseHref == 'string') {
let baseElement;
for (const node of headElement.childNodes) {
if (node.tagName === 'base') {
baseElement = node;
break;
for (const headChild of headElement.childNodes) {
if (headChild.tagName === 'base') {
baseElement = headChild;
}
}

const baseFragment = treeAdapter.createDocumentFragment();

if (!baseElement) {
const element = treeAdapter.createElement(
baseElement = treeAdapter.createElement(
'base',
undefined,
[
{ name: 'href', value: this._options.baseHref },
],
);
treeAdapter.appendChild(headElement, element);

treeAdapter.appendChild(baseFragment, baseElement);
indexSource.insert(
headElement.__location.startTag.endOffset + 1,
parse5.serialize(baseFragment, { treeAdapter }),
);
} else {
let hrefAttribute;
for (const attribute of baseElement.attrs) {
Expand All @@ -172,24 +195,51 @@ export class IndexHtmlWebpackPlugin {
} else {
baseElement.attrs.push({ name: 'href', value: this._options.baseHref });
}

treeAdapter.appendChild(baseFragment, baseElement);
indexSource.replace(
baseElement.__location.startOffset,
baseElement.__location.endOffset,
parse5.serialize(baseFragment, { treeAdapter }),
);
}
}

const styleElements = treeAdapter.createDocumentFragment();
for (const stylesheet of stylesheets) {
const element = treeAdapter.createElement(
'link',
undefined,
[
{ name: 'rel', value: 'stylesheet' },
{ name: 'href', value: (this._options.deployUrl || '') + stylesheet },
],
);
treeAdapter.appendChild(headElement, element);
const attrs = [
{ name: 'rel', value: 'stylesheet' },
{ name: 'href', value: (this._options.deployUrl || '') + stylesheet },
];

if (this._options.sri) {
const content = compilation.assets[stylesheet].source();
attrs.push(...this._generateSriAttributes(content));
}

const element = treeAdapter.createElement('link', undefined, attrs);
treeAdapter.appendChild(styleElements, element);
}

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

// Add to compilation assets
const outputContent = parse5.serialize(document, { treeAdapter });
compilation.assets[this._options.output] = new RawSource(outputContent);
compilation.assets[this._options.output] = indexSource;
});
}

private _generateSriAttributes(content: string) {
const algo = 'sha384';
const hash = createHash(algo)
.update(content, 'utf8')
.digest('base64');

return [
{ name: 'integrity', value: `${algo}-${hash}` },
{ name: 'crossorigin', value: 'anonymous' },
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import { join, normalize, virtualFs } from '@angular-devkit/core';
import { join, normalize, tags, virtualFs } from '@angular-devkit/core';
import { tap } from 'rxjs/operators';
import { Timeout, browserTargetSpec, host, runTargetSpec } from '../utils';

Expand Down Expand Up @@ -52,4 +52,42 @@ describe('Browser Builder works with BOM index.html', () => {
}),
).subscribe(undefined, done.fail, done);
}, Timeout.Basic);

it('keeps escaped charaters', (done) => {
host.writeMultipleFiles({
'src/index.html': tags.oneLine`
<html><head><title>&iacute;</title><base href="/"></head>
<body><app-root></app-root></body></html>
`,
});

runTargetSpec(host, browserTargetSpec).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
tap(() => {
const fileName = join(outputPath, 'index.html');
const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName)));
// tslint:disable-next-line:max-line-length
expect(content).toBe(`<html><head><title>&iacute;</title><base href="/"></head> <body><app-root></app-root><script type="text/javascript" src="runtime.js"></script><script type="text/javascript" src="polyfills.js"></script><script type="text/javascript" src="styles.js"></script><script type="text/javascript" src="vendor.js"></script><script type="text/javascript" src="main.js"></script></body></html>`);
}),
).subscribe(undefined, done.fail, done);
}, Timeout.Basic);

it('keeps custom template charaters', (done) => {
host.writeMultipleFiles({
'src/index.html': tags.oneLine`
<html><head><base href="/"><%= csrf_meta_tags %></head>
<body><app-root></app-root></body></html>
`,
});

runTargetSpec(host, browserTargetSpec).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
tap(() => {
const fileName = join(outputPath, 'index.html');
const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName)));
// tslint:disable-next-line:max-line-length
expect(content).toBe(`<html><head><base href="/"><%= csrf_meta_tags %></head> <body><app-root></app-root><script type="text/javascript" src="runtime.js"></script><script type="text/javascript" src="polyfills.js"></script><script type="text/javascript" src="styles.js"></script><script type="text/javascript" src="vendor.js"></script><script type="text/javascript" src="main.js"></script></body></html>`);
}),
).subscribe(undefined, done.fail, done);
}, Timeout.Basic);
});
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('Browser Builder', () => {
hashTable: {
'/favicon.ico': '84161b857f5c547e3699ddfbffc6d8d737542e01',
'/assets/folder-asset.txt': '617f202968a6a81050aa617c2e28e1dca11ce8d4',
'/index.html': '3e659d6e536916b7d178d02a2e6e5492f868bf68',
'/index.html': '843c96f0aeadc8f093b1b2203c08891ecd8f7425',
},
});
}),
Expand Down Expand Up @@ -153,7 +153,7 @@ describe('Browser Builder', () => {
hashTable: {
'/foo/bar/favicon.ico': '84161b857f5c547e3699ddfbffc6d8d737542e01',
'/foo/bar/assets/folder-asset.txt': '617f202968a6a81050aa617c2e28e1dca11ce8d4',
'/foo/bar/index.html': '5b53fa9e07e4111b8ef84613fb989a56fee502b0',
'/foo/bar/index.html': '9ef50361678004b3b197c12cbc74962e5a15b844',
},
});
}),
Expand Down

0 comments on commit a820750

Please sign in to comment.