Skip to content

Commit 0393429

Browse files
clydinfilipesilva
authored andcommitted
feat(@angular/cli): add index html plugin
1 parent aeb8ea2 commit 0393429

File tree

7 files changed

+197
-15
lines changed

7 files changed

+197
-15
lines changed

package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"node-modules-path": "^1.0.0",
7575
"nopt": "^4.0.1",
7676
"opn": "~5.1.0",
77+
"parse5": "^4.0.0",
7778
"portfinder": "~1.0.12",
7879
"postcss": "^6.0.16",
7980
"postcss-import": "^11.0.0",

packages/@angular/cli/models/webpack-configs/browser.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import * as path from 'path';
33
const HtmlWebpackPlugin = require('html-webpack-plugin');
44
const SubresourceIntegrityPlugin = require('webpack-subresource-integrity');
55

6-
import { packageChunkSort } from '../../utilities/package-chunk-sort';
6+
import { generateEntryPoints, packageChunkSort } from '../../utilities/package-chunk-sort';
77
import { BaseHrefWebpackPlugin } from '../../lib/base-href-webpack';
8+
import { IndexHtmlWebpackPlugin } from '../../plugins/index-html-webpack-plugin';
89
import { extraEntryParser, lazyChunksFilter } from './utils';
910
import { WebpackConfigOptions } from '../webpack-config';
1011

@@ -99,7 +100,15 @@ export function getBrowserConfig(wco: WebpackConfigOptions) {
99100
}
100101
}
101102
},
102-
plugins: extraPlugins,
103+
plugins: extraPlugins.concat([
104+
new IndexHtmlWebpackPlugin({
105+
input: path.resolve(appRoot, appConfig.index),
106+
output: appConfig.index,
107+
baseHref: buildOptions.baseHref,
108+
entrypoints: generateEntryPoints(appConfig),
109+
deployUrl: buildOptions.deployUrl,
110+
}),
111+
]),
103112
node: {
104113
fs: 'empty',
105114
global: true,

packages/@angular/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"node-modules-path": "^1.0.0",
6161
"nopt": "^4.0.1",
6262
"opn": "~5.1.0",
63+
"parse5": "^4.0.0",
6364
"portfinder": "~1.0.12",
6465
"postcss": "^6.0.16",
6566
"postcss-import": "^11.0.0",
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { RawSource } from 'webpack-sources';
2+
3+
const parse5 = require('parse5');
4+
5+
export interface IndexHtmlWebpackPluginOptions {
6+
input: string;
7+
output: string;
8+
baseHref?: string;
9+
entrypoints: string[];
10+
deployUrl?: string;
11+
}
12+
13+
function readFile(filename: string, compilation: any): Promise<string> {
14+
return new Promise<string>((resolve, reject) => {
15+
compilation.inputFileSystem.readFile(filename, (err: Error, data: Buffer) => {
16+
if (err) {
17+
reject(err);
18+
return;
19+
}
20+
21+
const content = data.toString();
22+
resolve(content);
23+
});
24+
});
25+
}
26+
27+
export class IndexHtmlWebpackPlugin {
28+
private _options: IndexHtmlWebpackPluginOptions;
29+
30+
constructor(options?: Partial<IndexHtmlWebpackPluginOptions>) {
31+
this._options = {
32+
input: 'index.html',
33+
output: 'index.html',
34+
entrypoints: ['polyfills', 'main'],
35+
...options
36+
};
37+
}
38+
39+
apply(compiler: any) {
40+
compiler.hooks.emit.tapPromise('index-html-webpack-plugin', async (compilation: any) => {
41+
// Get input html file
42+
const inputContent = await readFile(this._options.input, compilation);
43+
compilation.fileDependencies.add(this._options.input);
44+
45+
46+
// Get all files for selected entrypoints
47+
const unfilteredSortedFiles: string[] = [];
48+
for (const entryName of this._options.entrypoints) {
49+
const entrypoint = compilation.entrypoints.get(entryName);
50+
if (entrypoint) {
51+
unfilteredSortedFiles.push(...entrypoint.getFiles());
52+
}
53+
}
54+
55+
// Filter files
56+
const existingFiles = new Set<string>();
57+
const stylesheets: string[] = [];
58+
const scripts: string[] = [];
59+
for (const file of unfilteredSortedFiles) {
60+
if (existingFiles.has(file)) {
61+
continue;
62+
}
63+
existingFiles.add(file);
64+
65+
if (file.endsWith('.js')) {
66+
scripts.push(file);
67+
} else if (file.endsWith('.css')) {
68+
stylesheets.push(file);
69+
}
70+
71+
}
72+
73+
// Find the head and body elements
74+
const treeAdapter = parse5.treeAdapters.default;
75+
const document = parse5.parse(inputContent, { treeAdapter });
76+
let headElement;
77+
let bodyElement;
78+
for (const topNode of document.childNodes) {
79+
if (topNode.tagName === 'html') {
80+
for (const htmlNode of topNode.childNodes) {
81+
if (htmlNode.tagName === 'head') {
82+
headElement = htmlNode;
83+
}
84+
if (htmlNode.tagName === 'body') {
85+
bodyElement = htmlNode;
86+
}
87+
}
88+
}
89+
}
90+
91+
// Inject into the html
92+
93+
if (!headElement || !bodyElement) {
94+
throw new Error('Missing head and/or body elements');
95+
}
96+
97+
for (const script of scripts) {
98+
const element = treeAdapter.createElement(
99+
'script',
100+
undefined,
101+
[
102+
{ name: 'type', value: 'text/javascript' },
103+
{ name: 'src', value: (this._options.deployUrl || '') + script },
104+
]
105+
);
106+
treeAdapter.appendChild(bodyElement, element);
107+
}
108+
109+
// Adjust base href if specified
110+
if (this._options.baseHref != undefined) {
111+
let baseElement;
112+
for (const node of headElement.childNodes) {
113+
if (node.tagName === 'base') {
114+
baseElement = node;
115+
break;
116+
}
117+
}
118+
119+
if (!baseElement) {
120+
const element = treeAdapter.createElement(
121+
'base',
122+
undefined,
123+
[
124+
{ name: 'href', value: this._options.baseHref },
125+
]
126+
);
127+
treeAdapter.appendChild(headElement, element);
128+
} else {
129+
let hrefAttribute;
130+
for (const attribute of baseElement.attrs) {
131+
if (attribute.name === 'href') {
132+
hrefAttribute = attribute;
133+
}
134+
}
135+
if (hrefAttribute) {
136+
hrefAttribute.value = this._options.baseHref;
137+
} else {
138+
baseElement.attrs.push({ name: 'href', value: this._options.baseHref });
139+
}
140+
}
141+
}
142+
143+
for (const stylesheet of stylesheets) {
144+
const element = treeAdapter.createElement(
145+
'link',
146+
undefined,
147+
[
148+
{ name: 'rel', value: 'stylesheet' },
149+
{ name: 'href', value: (this._options.deployUrl || '') + stylesheet },
150+
]
151+
);
152+
treeAdapter.appendChild(headElement, element);
153+
}
154+
155+
// Add to compilation assets
156+
const outputContent = parse5.serialize(document, { treeAdapter });
157+
compilation.assets[this._options.output] = new RawSource(outputContent);
158+
});
159+
}
160+
}

packages/@angular/cli/plugins/suppress-entry-chunks-webpack-plugin.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@ export class SuppressExtractedTextChunksWebpackPlugin {
3939
});
4040
// Remove scripts tags with a css file as source, because HtmlWebpackPlugin will use
4141
// a css file as a script for chunks without js files.
42-
compilation.plugin('html-webpack-plugin-alter-asset-tags',
43-
(htmlPluginData: any, callback: any) => {
44-
const filterFn = (tag: any) =>
45-
!(tag.tagName === 'script' && tag.attributes.src && tag.attributes.src.match(/\.css$/));
46-
htmlPluginData.head = htmlPluginData.head.filter(filterFn);
47-
htmlPluginData.body = htmlPluginData.body.filter(filterFn);
48-
callback(null, htmlPluginData);
49-
});
42+
// compilation.plugin('html-webpack-plugin-alter-asset-tags',
43+
// (htmlPluginData: any, callback: any) => {
44+
// const filterFn = (tag: any) =>
45+
// !(tag.tagName === 'script' && tag.attributes.src && tag.attributes.src.match(/\.css$/));
46+
// htmlPluginData.head = htmlPluginData.head.filter(filterFn);
47+
// htmlPluginData.body = htmlPluginData.body.filter(filterFn);
48+
// callback(null, htmlPluginData);
49+
// });
5050
});
5151
}
5252
}

packages/@angular/cli/utilities/package-chunk-sort.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { ExtraEntry, extraEntryParser } from '../models/webpack-configs/utils';
22

3-
// Sort chunks according to a predefined order:
4-
// inline, polyfills, all styles, vendor, main
5-
export function packageChunkSort(appConfig: any) {
6-
let entryPoints = ['inline', 'polyfills', 'sw-register'];
3+
export function generateEntryPoints(appConfig: any) {
4+
let entryPoints = ['polyfills', 'sw-register'];
75

86
const pushExtraEntries = (extraEntry: ExtraEntry) => {
97
if (entryPoints.indexOf(extraEntry.entry) === -1) {
@@ -23,7 +21,15 @@ export function packageChunkSort(appConfig: any) {
2321
.forEach(pushExtraEntries);
2422
}
2523

26-
entryPoints.push(...['vendor', 'main']);
24+
entryPoints.push('main');
25+
26+
return entryPoints;
27+
}
28+
29+
// Sort chunks according to a predefined order:
30+
// inline, polyfills, all styles, vendor, main
31+
export function packageChunkSort(appConfig: any) {
32+
const entryPoints = generateEntryPoints(appConfig);
2733

2834
function sort(left: any, right: any) {
2935
let leftIndex = entryPoints.indexOf(left.names[0]);

0 commit comments

Comments
 (0)