Skip to content

Commit 409247e

Browse files
alan-agius4clydin
authored andcommitted
refactor(@angular-devkit/build-angular): use parse5-html-rewriting-stream instead of parse5-htmlparser2-tree-adapter
Closes: #17019
1 parent 2f2690b commit 409247e

File tree

6 files changed

+159
-125
lines changed

6 files changed

+159
-125
lines changed

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
"@types/node": "10.12.30",
111111
"@types/node-fetch": "^2.1.6",
112112
"@types/npm-package-arg": "^6.1.0",
113+
"@types/parse5-html-rewriting-stream": "^5.1.2",
113114
"@types/pidusage": "^2.0.1",
114115
"@types/progress": "^2.0.3",
115116
"@types/request": "^2.47.1",
@@ -179,9 +180,7 @@
179180
"open": "7.3.0",
180181
"ora": "5.1.0",
181182
"pacote": "11.1.4",
182-
"parse5": "6.0.1",
183183
"parse5-html-rewriting-stream": "6.0.1",
184-
"parse5-htmlparser2-tree-adapter": "6.0.1",
185184
"pidtree": "^0.5.0",
186185
"pidusage": "^2.0.17",
187186
"pnp-webpack-plugin": "1.6.4",

packages/angular_devkit/build_angular/BUILD.bazel

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ ts_library(
125125
"@npm//@types/loader-utils",
126126
"@npm//@types/minimatch",
127127
"@npm//@types/node",
128+
"@npm//@types/parse5-html-rewriting-stream",
128129
"@npm//@types/rimraf",
129130
"@npm//@types/semver",
130131
"@npm//@types/speed-measure-webpack-plugin",
@@ -160,8 +161,7 @@ ts_library(
160161
"@npm//ng-packagr",
161162
"@npm//open",
162163
"@npm//ora",
163-
"@npm//parse5",
164-
"@npm//parse5-htmlparser2-tree-adapter",
164+
"@npm//parse5-html-rewriting-stream",
165165
"@npm//pnp-webpack-plugin",
166166
"@npm//postcss",
167167
"@npm//postcss-import",

packages/angular_devkit/build_angular/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,7 @@
4444
"minimatch": "3.0.4",
4545
"open": "7.3.0",
4646
"ora": "5.1.0",
47-
"parse5": "6.0.1",
48-
"parse5-htmlparser2-tree-adapter": "6.0.1",
47+
"parse5-html-rewriting-stream": "6.0.1",
4948
"pnp-webpack-plugin": "1.6.4",
5049
"postcss": "7.0.32",
5150
"postcss-import": "12.0.1",

packages/angular_devkit/build_angular/src/utils/index-file/augment-index-html.ts

Lines changed: 89 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@
77
*/
88

99
import { createHash } from 'crypto';
10-
import { RawSource, ReplaceSource } from 'webpack-sources';
11-
12-
const parse5 = require('parse5');
13-
const treeAdapter = require('parse5-htmlparser2-tree-adapter');
10+
import { htmlRewritingStream } from './html-rewriting-stream';
1411

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

@@ -59,12 +56,14 @@ export interface FileInfo {
5956
* after processing several configurations in order to build different sets of
6057
* bundles for differential serving.
6158
*/
62-
// tslint:disable-next-line: no-big-function
6359
export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise<string> {
64-
const { loadOutputFile, files, noModuleFiles = [], moduleFiles = [], entrypoints } = params;
60+
const {
61+
loadOutputFile, files, noModuleFiles = [], moduleFiles = [], entrypoints,
62+
sri, deployUrl = '', lang, baseHref, inputContent,
63+
} = params;
6564

6665
let { crossOrigin = 'none' } = params;
67-
if (params.sri && crossOrigin === 'none') {
66+
if (sri && crossOrigin === 'none') {
6867
crossOrigin = 'anonymous';
6968
}
7069

@@ -90,33 +89,12 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
9089
}
9190
}
9291

93-
// Find the head and body elements
94-
const document = parse5.parse(params.inputContent, {
95-
treeAdapter,
96-
sourceCodeLocationInfo: true,
97-
});
98-
99-
// tslint:disable: no-any
100-
const htmlElement = document.children.find((c: any) => c.name === 'html');
101-
const headElement = htmlElement.children.find((c: any) => c.name === 'head');
102-
const bodyElement = htmlElement.children.find((c: any) => c.name === 'body');
103-
// tslint:enable: no-any
104-
105-
if (!headElement || !bodyElement) {
106-
throw new Error('Missing head and/or body elements');
107-
}
108-
109-
// Inject into the html
110-
const indexSource = new ReplaceSource(new RawSource(params.inputContent), params.input);
111-
112-
const scriptsElements = treeAdapter.createDocumentFragment();
92+
const scriptTags: string[] = [];
11393
for (const script of scripts) {
114-
const attrs: { name: string; value: string }[] = [
115-
{ name: 'src', value: (params.deployUrl || '') + script },
116-
];
94+
const attrs = [`src="${deployUrl}${script}"`];
11795

11896
if (crossOrigin !== 'none') {
119-
attrs.push({ name: 'crossorigin', value: crossOrigin });
97+
attrs.push(`crossorigin="${crossOrigin}"`);
12098
}
12199

122100
// We want to include nomodule or module when a file is not common amongs all
@@ -130,111 +108,115 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
130108
const isModuleType = moduleFiles.some(scriptPredictor);
131109

132110
if (isNoModuleType && !isModuleType) {
133-
attrs.push(
134-
{ name: 'nomodule', value: '' },
135-
{ name: 'defer', value: '' },
136-
);
111+
attrs.push('nomodule', 'defer');
137112
} else if (isModuleType && !isNoModuleType) {
138-
attrs.push({ name: 'type', value: 'module' });
113+
attrs.push('type="module"');
139114
} else {
140-
attrs.push({ name: 'defer', value: '' });
115+
attrs.push('defer');
141116
}
142117
} else {
143-
attrs.push({ name: 'defer', value: '' });
118+
attrs.push('defer');
144119
}
145120

146-
if (params.sri) {
121+
if (sri) {
147122
const content = await loadOutputFile(script);
148-
attrs.push(_generateSriAttributes(content));
123+
attrs.push(generateSriAttributes(content));
149124
}
150125

151-
const baseElement = treeAdapter.createElement('script', undefined, attrs);
152-
treeAdapter.setTemplateContent(scriptsElements, baseElement);
126+
scriptTags.push(`<script ${attrs.join(' ')}></script>`);
153127
}
154128

155-
indexSource.insert(
156-
// parse5 does not provide locations if malformed html is present
157-
bodyElement.sourceCodeLocation?.endTag?.startOffset || params.inputContent.indexOf('</body>'),
158-
parse5.serialize(scriptsElements, { treeAdapter }).replace(/\=""/g, ''),
159-
);
160-
161-
// Adjust base href if specified
162-
if (typeof params.baseHref == 'string') {
163-
// tslint:disable-next-line: no-any
164-
let baseElement = headElement.children.find((t: any) => t.name === 'base');
165-
const baseFragment = treeAdapter.createDocumentFragment();
166-
167-
if (!baseElement) {
168-
baseElement = treeAdapter.createElement('base', undefined, [
169-
{ name: 'href', value: params.baseHref },
170-
]);
171-
172-
treeAdapter.setTemplateContent(baseFragment, baseElement);
173-
indexSource.insert(
174-
headElement.sourceCodeLocation.startTag.endOffset,
175-
parse5.serialize(baseFragment, { treeAdapter }),
176-
);
177-
} else {
178-
baseElement.attribs['href'] = params.baseHref;
179-
treeAdapter.setTemplateContent(baseFragment, baseElement);
180-
indexSource.replace(
181-
baseElement.sourceCodeLocation.startOffset,
182-
baseElement.sourceCodeLocation.endOffset - 1,
183-
parse5.serialize(baseFragment, { treeAdapter }),
184-
);
185-
}
186-
}
187-
188-
const styleElements = treeAdapter.createDocumentFragment();
129+
const linkTags: string[] = [];
189130
for (const stylesheet of stylesheets) {
190131
const attrs = [
191-
{ name: 'rel', value: 'stylesheet' },
192-
{ name: 'href', value: (params.deployUrl || '') + stylesheet },
132+
`rel="stylesheet"`,
133+
`href="${deployUrl}${stylesheet}"`,
193134
];
194135

195136
if (crossOrigin !== 'none') {
196-
attrs.push({ name: 'crossorigin', value: crossOrigin });
137+
attrs.push(`crossorigin="${crossOrigin}"`);
197138
}
198139

199-
if (params.sri) {
140+
if (sri) {
200141
const content = await loadOutputFile(stylesheet);
201-
attrs.push(_generateSriAttributes(content));
142+
attrs.push(generateSriAttributes(content));
202143
}
203144

204-
const element = treeAdapter.createElement('link', undefined, attrs);
205-
treeAdapter.setTemplateContent(styleElements, element);
145+
linkTags.push(`<link ${attrs.join(' ')}>`);
206146
}
207147

208-
indexSource.insert(
209-
// parse5 does not provide locations if malformed html is present
210-
headElement.sourceCodeLocation?.endTag?.startOffset || params.inputContent.indexOf('</head>'),
211-
parse5.serialize(styleElements, { treeAdapter }),
212-
);
213-
214-
// Adjust document locale if specified
215-
if (typeof params.lang == 'string') {
216-
const htmlFragment = treeAdapter.createDocumentFragment();
217-
htmlElement.attribs['lang'] = params.lang;
218-
219-
// we want only openning tag
220-
htmlElement.children = [];
221-
222-
treeAdapter.setTemplateContent(htmlFragment, htmlElement);
223-
indexSource.replace(
224-
htmlElement.sourceCodeLocation.startTag.startOffset,
225-
htmlElement.sourceCodeLocation.startTag.endOffset - 1,
226-
parse5.serialize(htmlFragment, { treeAdapter }).replace('</html>', ''),
227-
);
228-
}
148+
const { rewriter, transformedContent } = await htmlRewritingStream(inputContent);
149+
const baseTagExists = inputContent.includes('<base');
150+
151+
rewriter
152+
.on('startTag', tag => {
153+
switch (tag.tagName) {
154+
case 'html':
155+
// Adjust document locale if specified
156+
if (isString(lang)) {
157+
updateAttribute(tag, 'lang', lang);
158+
}
159+
break;
160+
case 'head':
161+
// Base href should be added before any link, meta tags
162+
if (!baseTagExists && isString(baseHref)) {
163+
rewriter.emitStartTag(tag);
164+
rewriter.emitRaw(`<base href="${baseHref}">`);
165+
166+
return;
167+
}
168+
break;
169+
case 'base':
170+
// Adjust base href if specified
171+
if (isString(baseHref)) {
172+
updateAttribute(tag, 'href', baseHref);
173+
}
174+
break;
175+
}
176+
177+
rewriter.emitStartTag(tag);
178+
})
179+
.on('endTag', tag => {
180+
switch (tag.tagName) {
181+
case 'head':
182+
for (const linkTag of linkTags) {
183+
rewriter.emitRaw(linkTag);
184+
}
185+
break;
186+
case 'body':
187+
// Add script tags
188+
for (const scriptTag of scriptTags) {
189+
rewriter.emitRaw(scriptTag);
190+
}
191+
break;
192+
}
193+
194+
rewriter.emitEndTag(tag);
195+
});
229196

230-
return indexSource.source();
197+
return transformedContent;
231198
}
232199

233-
function _generateSriAttributes(content: string) {
200+
function generateSriAttributes(content: string): string {
234201
const algo = 'sha384';
235202
const hash = createHash(algo)
236203
.update(content, 'utf8')
237204
.digest('base64');
238205

239-
return { name: 'integrity', value: `${algo}-${hash}` };
206+
return `integrity="${algo}-${hash}"`;
207+
}
208+
209+
function updateAttribute(tag: { attrs: { name: string, value: string }[] }, name: string, value: string): void {
210+
const index = tag.attrs.findIndex(a => a.name === name);
211+
const newValue = { name, value };
212+
213+
if (index === -1) {
214+
tag.attrs.push(newValue);
215+
} else {
216+
tag.attrs[index] = newValue;
217+
}
218+
}
219+
220+
function isString(value: unknown): value is string {
221+
return typeof value === 'string';
240222
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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 { Readable, Writable } from 'stream';
10+
11+
export async function htmlRewritingStream(content: string): Promise<{
12+
rewriter: import('parse5-html-rewriting-stream'),
13+
transformedContent: Promise<string>,
14+
}> {
15+
const chunks: Buffer[] = [];
16+
const rewriter = new (await import('parse5-html-rewriting-stream'))();
17+
18+
return {
19+
rewriter,
20+
transformedContent: new Promise(resolve => {
21+
new Readable({
22+
encoding: 'utf8',
23+
read(): void {
24+
this.push(Buffer.from(content));
25+
this.push(null);
26+
},
27+
})
28+
.pipe(rewriter)
29+
.pipe(new Writable({
30+
write(chunk: string | Buffer, encoding: string | undefined, callback: Function): void {
31+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, encoding) : chunk);
32+
callback();
33+
},
34+
final(callback: (error?: Error) => void): void {
35+
callback();
36+
resolve(Buffer.concat(chunks).toString());
37+
},
38+
}));
39+
}),
40+
};
41+
}

0 commit comments

Comments
 (0)