html-webpack-plugin 5.5.3 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/index.js +823 -791
- package/lib/cached-child-compiler.js +78 -21
- package/lib/child-compiler.js +46 -25
- package/lib/chunksorter.js +8 -7
- package/lib/hooks.js +2 -2
- package/package.json +10 -1
- package/typings.d.ts +3 -3
- package/lib/file-watcher-api.js +0 -71
package/index.js
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
// Import types
|
|
3
|
-
/** @typedef {import("./typings").HtmlTagObject} HtmlTagObject */
|
|
4
|
-
/** @typedef {import("./typings").Options} HtmlWebpackOptions */
|
|
5
|
-
/** @typedef {import("./typings").ProcessedOptions} ProcessedHtmlWebpackOptions */
|
|
6
|
-
/** @typedef {import("./typings").TemplateParameter} TemplateParameter */
|
|
7
|
-
/** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */
|
|
8
|
-
/** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
|
|
9
2
|
'use strict';
|
|
10
3
|
|
|
11
4
|
const promisify = require('util').promisify;
|
|
@@ -17,13 +10,19 @@ const path = require('path');
|
|
|
17
10
|
const { CachedChildCompilation } = require('./lib/cached-child-compiler');
|
|
18
11
|
|
|
19
12
|
const { createHtmlTagObject, htmlTagObjectToString, HtmlTagArray } = require('./lib/html-tags');
|
|
20
|
-
|
|
21
13
|
const prettyError = require('./lib/errors.js');
|
|
22
14
|
const chunkSorter = require('./lib/chunksorter.js');
|
|
23
15
|
const getHtmlWebpackPluginHooks = require('./lib/hooks.js').getHtmlWebpackPluginHooks;
|
|
24
|
-
const { assert } = require('console');
|
|
25
16
|
|
|
26
|
-
|
|
17
|
+
/** @typedef {import("./typings").HtmlTagObject} HtmlTagObject */
|
|
18
|
+
/** @typedef {import("./typings").Options} HtmlWebpackOptions */
|
|
19
|
+
/** @typedef {import("./typings").ProcessedOptions} ProcessedHtmlWebpackOptions */
|
|
20
|
+
/** @typedef {import("./typings").TemplateParameter} TemplateParameter */
|
|
21
|
+
/** @typedef {import("webpack").Compiler} Compiler */
|
|
22
|
+
/** @typedef {ReturnType<Compiler["getInfrastructureLogger"]>} Logger */
|
|
23
|
+
/** @typedef {import("webpack/lib/Compilation.js")} Compilation */
|
|
24
|
+
/** @typedef {Array<{ name: string, source: import('webpack').sources.Source, info?: import('webpack').AssetInfo }>} PreviousEmittedAssets */
|
|
25
|
+
/** @typedef {{ publicPath: string, js: Array<string>, css: Array<string>, manifest?: string, favicon?: string }} AssetsInformationByGroups */
|
|
27
26
|
|
|
28
27
|
class HtmlWebpackPlugin {
|
|
29
28
|
/**
|
|
@@ -31,60 +30,89 @@ class HtmlWebpackPlugin {
|
|
|
31
30
|
*/
|
|
32
31
|
constructor (options) {
|
|
33
32
|
/** @type {HtmlWebpackOptions} */
|
|
33
|
+
// TODO remove me in the next major release
|
|
34
34
|
this.userOptions = options || {};
|
|
35
35
|
this.version = HtmlWebpackPlugin.version;
|
|
36
|
+
|
|
37
|
+
// Default options
|
|
38
|
+
/** @type {ProcessedHtmlWebpackOptions} */
|
|
39
|
+
const defaultOptions = {
|
|
40
|
+
template: 'auto',
|
|
41
|
+
templateContent: false,
|
|
42
|
+
templateParameters: templateParametersGenerator,
|
|
43
|
+
filename: 'index.html',
|
|
44
|
+
publicPath: this.userOptions.publicPath === undefined ? 'auto' : this.userOptions.publicPath,
|
|
45
|
+
hash: false,
|
|
46
|
+
inject: this.userOptions.scriptLoading === 'blocking' ? 'body' : 'head',
|
|
47
|
+
scriptLoading: 'defer',
|
|
48
|
+
compile: true,
|
|
49
|
+
favicon: false,
|
|
50
|
+
minify: 'auto',
|
|
51
|
+
cache: true,
|
|
52
|
+
showErrors: true,
|
|
53
|
+
chunks: 'all',
|
|
54
|
+
excludeChunks: [],
|
|
55
|
+
chunksSortMode: 'auto',
|
|
56
|
+
meta: {},
|
|
57
|
+
base: false,
|
|
58
|
+
title: 'Webpack App',
|
|
59
|
+
xhtml: false
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** @type {ProcessedHtmlWebpackOptions} */
|
|
63
|
+
this.options = Object.assign(defaultOptions, this.userOptions);
|
|
36
64
|
}
|
|
37
65
|
|
|
66
|
+
/**
|
|
67
|
+
*
|
|
68
|
+
* @param {Compiler} compiler
|
|
69
|
+
* @returns {void}
|
|
70
|
+
*/
|
|
38
71
|
apply (compiler) {
|
|
72
|
+
this.logger = compiler.getInfrastructureLogger('HtmlWebpackPlugin');
|
|
73
|
+
|
|
39
74
|
// Wait for configuration preset plugions to apply all configure webpack defaults
|
|
40
75
|
compiler.hooks.initialize.tap('HtmlWebpackPlugin', () => {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
// Default options
|
|
44
|
-
/** @type {ProcessedHtmlWebpackOptions} */
|
|
45
|
-
const defaultOptions = {
|
|
46
|
-
template: 'auto',
|
|
47
|
-
templateContent: false,
|
|
48
|
-
templateParameters: templateParametersGenerator,
|
|
49
|
-
filename: 'index.html',
|
|
50
|
-
publicPath: userOptions.publicPath === undefined ? 'auto' : userOptions.publicPath,
|
|
51
|
-
hash: false,
|
|
52
|
-
inject: userOptions.scriptLoading === 'blocking' ? 'body' : 'head',
|
|
53
|
-
scriptLoading: 'defer',
|
|
54
|
-
compile: true,
|
|
55
|
-
favicon: false,
|
|
56
|
-
minify: 'auto',
|
|
57
|
-
cache: true,
|
|
58
|
-
showErrors: true,
|
|
59
|
-
chunks: 'all',
|
|
60
|
-
excludeChunks: [],
|
|
61
|
-
chunksSortMode: 'auto',
|
|
62
|
-
meta: {},
|
|
63
|
-
base: false,
|
|
64
|
-
title: 'Webpack App',
|
|
65
|
-
xhtml: false
|
|
66
|
-
};
|
|
76
|
+
const options = this.options;
|
|
67
77
|
|
|
68
|
-
|
|
69
|
-
const options = Object.assign(defaultOptions, userOptions);
|
|
70
|
-
this.options = options;
|
|
78
|
+
options.template = this.getTemplatePath(this.options.template, compiler.context);
|
|
71
79
|
|
|
72
80
|
// Assert correct option spelling
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
if (options.scriptLoading !== 'defer' && options.scriptLoading !== 'blocking' && options.scriptLoading !== 'module' && options.scriptLoading !== 'systemjs-module') {
|
|
82
|
+
/** @type {Logger} */
|
|
83
|
+
(this.logger).error('The "scriptLoading" option need to be set to "defer", "blocking" or "module" or "systemjs-module"');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (options.inject !== true && options.inject !== false && options.inject !== 'head' && options.inject !== 'body') {
|
|
87
|
+
/** @type {Logger} */
|
|
88
|
+
(this.logger).error('The `inject` option needs to be set to true, false, "head" or "body');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (
|
|
92
|
+
this.options.templateParameters !== false &&
|
|
93
|
+
typeof this.options.templateParameters !== 'function' &&
|
|
94
|
+
typeof this.options.templateParameters !== 'object'
|
|
95
|
+
) {
|
|
96
|
+
/** @type {Logger} */
|
|
97
|
+
(this.logger).error('The `templateParameters` has to be either a function or an object or false');
|
|
98
|
+
}
|
|
75
99
|
|
|
76
100
|
// Default metaOptions if no template is provided
|
|
77
|
-
if (!userOptions.template && options.templateContent === false && options.meta) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
101
|
+
if (!this.userOptions.template && options.templateContent === false && options.meta) {
|
|
102
|
+
options.meta = Object.assign(
|
|
103
|
+
{},
|
|
104
|
+
options.meta,
|
|
105
|
+
{
|
|
106
|
+
// TODO remove in the next major release
|
|
107
|
+
// From https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag
|
|
108
|
+
viewport: 'width=device-width, initial-scale=1'
|
|
109
|
+
},
|
|
110
|
+
this.userOptions.meta
|
|
111
|
+
);
|
|
84
112
|
}
|
|
85
113
|
|
|
86
114
|
// entryName to fileName conversion function
|
|
87
|
-
const userOptionFilename = userOptions.filename ||
|
|
115
|
+
const userOptionFilename = this.userOptions.filename || this.options.filename;
|
|
88
116
|
const filenameFunction = typeof userOptionFilename === 'function'
|
|
89
117
|
? userOptionFilename
|
|
90
118
|
// Replace '[name]' with entry name
|
|
@@ -94,19 +122,313 @@ class HtmlWebpackPlugin {
|
|
|
94
122
|
const entryNames = Object.keys(compiler.options.entry);
|
|
95
123
|
const outputFileNames = new Set((entryNames.length ? entryNames : ['main']).map(filenameFunction));
|
|
96
124
|
|
|
97
|
-
/** Option for every entry point */
|
|
98
|
-
const entryOptions = Array.from(outputFileNames).map((filename) => ({
|
|
99
|
-
...options,
|
|
100
|
-
filename
|
|
101
|
-
}));
|
|
102
|
-
|
|
103
125
|
// Hook all options into the webpack compiler
|
|
104
|
-
|
|
105
|
-
|
|
126
|
+
outputFileNames.forEach((outputFileName) => {
|
|
127
|
+
// Instance variables to keep caching information for multiple builds
|
|
128
|
+
const assetJson = { value: undefined };
|
|
129
|
+
/**
|
|
130
|
+
* store the previous generated asset to emit them even if the content did not change
|
|
131
|
+
* to support watch mode for third party plugins like the clean-webpack-plugin or the compression plugin
|
|
132
|
+
* @type {PreviousEmittedAssets}
|
|
133
|
+
*/
|
|
134
|
+
const previousEmittedAssets = [];
|
|
135
|
+
|
|
136
|
+
// Inject child compiler plugin
|
|
137
|
+
const childCompilerPlugin = new CachedChildCompilation(compiler);
|
|
138
|
+
|
|
139
|
+
if (!this.options.templateContent) {
|
|
140
|
+
childCompilerPlugin.addEntry(this.options.template);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// convert absolute filename into relative so that webpack can
|
|
144
|
+
// generate it at correct location
|
|
145
|
+
let filename = outputFileName;
|
|
146
|
+
|
|
147
|
+
if (path.resolve(filename) === path.normalize(filename)) {
|
|
148
|
+
const outputPath = /** @type {string} - Once initialized the path is always a string */(compiler.options.output.path);
|
|
149
|
+
|
|
150
|
+
filename = path.relative(outputPath, filename);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
compiler.hooks.thisCompilation.tap('HtmlWebpackPlugin',
|
|
154
|
+
/**
|
|
155
|
+
* Hook into the webpack compilation
|
|
156
|
+
* @param {Compilation} compilation
|
|
157
|
+
*/
|
|
158
|
+
(compilation) => {
|
|
159
|
+
compilation.hooks.processAssets.tapAsync(
|
|
160
|
+
{
|
|
161
|
+
name: 'HtmlWebpackPlugin',
|
|
162
|
+
stage:
|
|
163
|
+
/**
|
|
164
|
+
* Generate the html after minification and dev tooling is done
|
|
165
|
+
*/
|
|
166
|
+
compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE
|
|
167
|
+
},
|
|
168
|
+
/**
|
|
169
|
+
* Hook into the process assets hook
|
|
170
|
+
* @param {any} _
|
|
171
|
+
* @param {(err?: Error) => void} callback
|
|
172
|
+
*/
|
|
173
|
+
(_, callback) => {
|
|
174
|
+
this.generateHTML(compiler, compilation, filename, childCompilerPlugin, previousEmittedAssets, assetJson, callback);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
106
177
|
});
|
|
107
178
|
});
|
|
108
179
|
}
|
|
109
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Helper to return the absolute template path with a fallback loader
|
|
183
|
+
*
|
|
184
|
+
* @private
|
|
185
|
+
* @param {string} template The path to the template e.g. './index.html'
|
|
186
|
+
* @param {string} context The webpack base resolution path for relative paths e.g. process.cwd()
|
|
187
|
+
*/
|
|
188
|
+
getTemplatePath (template, context) {
|
|
189
|
+
if (template === 'auto') {
|
|
190
|
+
template = path.resolve(context, 'src/index.ejs');
|
|
191
|
+
if (!fs.existsSync(template)) {
|
|
192
|
+
template = path.join(__dirname, 'default_index.ejs');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// If the template doesn't use a loader use the lodash template loader
|
|
197
|
+
if (template.indexOf('!') === -1) {
|
|
198
|
+
template = require.resolve('./lib/loader.js') + '!' + path.resolve(context, template);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Resolve template path
|
|
202
|
+
return template.replace(
|
|
203
|
+
/([!])([^/\\][^!?]+|[^/\\!?])($|\?[^!?\n]+$)/,
|
|
204
|
+
(match, prefix, filepath, postfix) => prefix + path.resolve(filepath) + postfix);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Return all chunks from the compilation result which match the exclude and include filters
|
|
209
|
+
*
|
|
210
|
+
* @private
|
|
211
|
+
* @param {any} chunks
|
|
212
|
+
* @param {string[]|'all'} includedChunks
|
|
213
|
+
* @param {string[]} excludedChunks
|
|
214
|
+
*/
|
|
215
|
+
filterEntryChunks (chunks, includedChunks, excludedChunks) {
|
|
216
|
+
return chunks.filter(chunkName => {
|
|
217
|
+
// Skip if the chunks should be filtered and the given chunk was not added explicity
|
|
218
|
+
if (Array.isArray(includedChunks) && includedChunks.indexOf(chunkName) === -1) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Skip if the chunks should be filtered and the given chunk was excluded explicity
|
|
223
|
+
if (Array.isArray(excludedChunks) && excludedChunks.indexOf(chunkName) !== -1) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Add otherwise
|
|
228
|
+
return true;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Helper to sort chunks
|
|
234
|
+
*
|
|
235
|
+
* @private
|
|
236
|
+
* @param {string[]} entryNames
|
|
237
|
+
* @param {string|((entryNameA: string, entryNameB: string) => number)} sortMode
|
|
238
|
+
* @param {Compilation} compilation
|
|
239
|
+
*/
|
|
240
|
+
sortEntryChunks (entryNames, sortMode, compilation) {
|
|
241
|
+
// Custom function
|
|
242
|
+
if (typeof sortMode === 'function') {
|
|
243
|
+
return entryNames.sort(sortMode);
|
|
244
|
+
}
|
|
245
|
+
// Check if the given sort mode is a valid chunkSorter sort mode
|
|
246
|
+
if (typeof chunkSorter[sortMode] !== 'undefined') {
|
|
247
|
+
return chunkSorter[sortMode](entryNames, compilation, this.options);
|
|
248
|
+
}
|
|
249
|
+
throw new Error('"' + sortMode + '" is not a valid chunk sort mode');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Encode each path component using `encodeURIComponent` as files can contain characters
|
|
254
|
+
* which needs special encoding in URLs like `+ `.
|
|
255
|
+
*
|
|
256
|
+
* Valid filesystem characters which need to be encoded for urls:
|
|
257
|
+
*
|
|
258
|
+
* # pound, % percent, & ampersand, { left curly bracket, } right curly bracket,
|
|
259
|
+
* \ back slash, < left angle bracket, > right angle bracket, * asterisk, ? question mark,
|
|
260
|
+
* blank spaces, $ dollar sign, ! exclamation point, ' single quotes, " double quotes,
|
|
261
|
+
* : colon, @ at sign, + plus sign, ` backtick, | pipe, = equal sign
|
|
262
|
+
*
|
|
263
|
+
* However the query string must not be encoded:
|
|
264
|
+
*
|
|
265
|
+
* fo:demonstration-path/very fancy+name.js?path=/home?value=abc&value=def#zzz
|
|
266
|
+
* ^ ^ ^ ^ ^ ^ ^ ^^ ^ ^ ^ ^ ^
|
|
267
|
+
* | | | | | | | || | | | | |
|
|
268
|
+
* encoded | | encoded | | || | | | | |
|
|
269
|
+
* ignored ignored ignored ignored ignored
|
|
270
|
+
*
|
|
271
|
+
* @private
|
|
272
|
+
* @param {string} filePath
|
|
273
|
+
*/
|
|
274
|
+
urlencodePath (filePath) {
|
|
275
|
+
// People use the filepath in quite unexpected ways.
|
|
276
|
+
// Try to extract the first querystring of the url:
|
|
277
|
+
//
|
|
278
|
+
// some+path/demo.html?value=abc?def
|
|
279
|
+
//
|
|
280
|
+
const queryStringStart = filePath.indexOf('?');
|
|
281
|
+
const urlPath = queryStringStart === -1 ? filePath : filePath.substr(0, queryStringStart);
|
|
282
|
+
const queryString = filePath.substr(urlPath.length);
|
|
283
|
+
// Encode all parts except '/' which are not part of the querystring:
|
|
284
|
+
const encodedUrlPath = urlPath.split('/').map(encodeURIComponent).join('/');
|
|
285
|
+
return encodedUrlPath + queryString;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Appends a cache busting hash to the query string of the url
|
|
290
|
+
* E.g. http://localhost:8080/ -> http://localhost:8080/?50c9096ba6183fd728eeb065a26ec175
|
|
291
|
+
*
|
|
292
|
+
* @private
|
|
293
|
+
* @param {string} url
|
|
294
|
+
* @param {string} hash
|
|
295
|
+
*/
|
|
296
|
+
appendHash (url, hash) {
|
|
297
|
+
if (!url) {
|
|
298
|
+
return url;
|
|
299
|
+
}
|
|
300
|
+
return url + (url.indexOf('?') === -1 ? '?' : '&') + hash;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Generate the relative or absolute base url to reference images, css, and javascript files
|
|
305
|
+
* from within the html file - the publicPath
|
|
306
|
+
*
|
|
307
|
+
* @private
|
|
308
|
+
* @param {Compilation} compilation
|
|
309
|
+
* @param {string} filename
|
|
310
|
+
* @param {string | 'auto'} customPublicPath
|
|
311
|
+
* @returns {string}
|
|
312
|
+
*/
|
|
313
|
+
getPublicPath (compilation, filename, customPublicPath) {
|
|
314
|
+
/**
|
|
315
|
+
* @type {string} the configured public path to the asset root
|
|
316
|
+
* if a path publicPath is set in the current webpack config use it otherwise
|
|
317
|
+
* fallback to a relative path
|
|
318
|
+
*/
|
|
319
|
+
const webpackPublicPath = compilation.getAssetPath(compilation.outputOptions.publicPath, { hash: compilation.hash });
|
|
320
|
+
// Webpack 5 introduced "auto" as default value
|
|
321
|
+
const isPublicPathDefined = webpackPublicPath !== 'auto';
|
|
322
|
+
|
|
323
|
+
let publicPath =
|
|
324
|
+
// If the html-webpack-plugin options contain a custom public path uset it
|
|
325
|
+
customPublicPath !== 'auto'
|
|
326
|
+
? customPublicPath
|
|
327
|
+
: (isPublicPathDefined
|
|
328
|
+
// If a hard coded public path exists use it
|
|
329
|
+
? webpackPublicPath
|
|
330
|
+
// If no public path was set get a relative url path
|
|
331
|
+
: path.relative(path.resolve(compilation.options.output.path, path.dirname(filename)), compilation.options.output.path)
|
|
332
|
+
.split(path.sep).join('/')
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
if (publicPath.length && publicPath.substr(-1, 1) !== '/') {
|
|
336
|
+
publicPath += '/';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return publicPath;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* The getAssetsForHTML extracts the asset information of a webpack compilation for all given entry names.
|
|
344
|
+
*
|
|
345
|
+
* @private
|
|
346
|
+
* @param {Compilation} compilation
|
|
347
|
+
* @param {string} outputName
|
|
348
|
+
* @param {string[]} entryNames
|
|
349
|
+
* @returns {AssetsInformationByGroups}
|
|
350
|
+
*/
|
|
351
|
+
getAssetsInformationByGroups (compilation, outputName, entryNames) {
|
|
352
|
+
/** The public path used inside the html file */
|
|
353
|
+
const publicPath = this.getPublicPath(compilation, outputName, this.options.publicPath);
|
|
354
|
+
/**
|
|
355
|
+
* @type {AssetsInformationByGroups}
|
|
356
|
+
*/
|
|
357
|
+
const assets = {
|
|
358
|
+
// The public path
|
|
359
|
+
publicPath,
|
|
360
|
+
// Will contain all js and mjs files
|
|
361
|
+
js: [],
|
|
362
|
+
// Will contain all css files
|
|
363
|
+
css: [],
|
|
364
|
+
// Will contain the html5 appcache manifest files if it exists
|
|
365
|
+
manifest: Object.keys(compilation.assets).find(assetFile => path.extname(assetFile) === '.appcache'),
|
|
366
|
+
// Favicon
|
|
367
|
+
favicon: undefined
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// Append a hash for cache busting
|
|
371
|
+
if (this.options.hash && assets.manifest) {
|
|
372
|
+
assets.manifest = this.appendHash(assets.manifest, /** @type {string} */ (compilation.hash));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Extract paths to .js, .mjs and .css files from the current compilation
|
|
376
|
+
const entryPointPublicPathMap = {};
|
|
377
|
+
const extensionRegexp = /\.(css|js|mjs)(\?|$)/;
|
|
378
|
+
|
|
379
|
+
for (let i = 0; i < entryNames.length; i++) {
|
|
380
|
+
const entryName = entryNames[i];
|
|
381
|
+
/** entryPointUnfilteredFiles - also includes hot module update files */
|
|
382
|
+
const entryPointUnfilteredFiles = compilation.entrypoints.get(entryName).getFiles();
|
|
383
|
+
const entryPointFiles = entryPointUnfilteredFiles.filter((chunkFile) => {
|
|
384
|
+
const asset = compilation.getAsset(chunkFile);
|
|
385
|
+
|
|
386
|
+
if (!asset) {
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Prevent hot-module files from being included:
|
|
391
|
+
const assetMetaInformation = asset.info || {};
|
|
392
|
+
|
|
393
|
+
return !(assetMetaInformation.hotModuleReplacement || assetMetaInformation.development);
|
|
394
|
+
});
|
|
395
|
+
// Prepend the publicPath and append the hash depending on the
|
|
396
|
+
// webpack.output.publicPath and hashOptions
|
|
397
|
+
// E.g. bundle.js -> /bundle.js?hash
|
|
398
|
+
const entryPointPublicPaths = entryPointFiles
|
|
399
|
+
.map(chunkFile => {
|
|
400
|
+
const entryPointPublicPath = publicPath + this.urlencodePath(chunkFile);
|
|
401
|
+
return this.options.hash
|
|
402
|
+
? this.appendHash(entryPointPublicPath, compilation.hash)
|
|
403
|
+
: entryPointPublicPath;
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
entryPointPublicPaths.forEach((entryPointPublicPath) => {
|
|
407
|
+
const extMatch = extensionRegexp.exec(entryPointPublicPath);
|
|
408
|
+
|
|
409
|
+
// Skip if the public path is not a .css, .mjs or .js file
|
|
410
|
+
if (!extMatch) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Skip if this file is already known
|
|
415
|
+
// (e.g. because of common chunk optimizations)
|
|
416
|
+
if (entryPointPublicPathMap[entryPointPublicPath]) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
entryPointPublicPathMap[entryPointPublicPath] = true;
|
|
421
|
+
|
|
422
|
+
// ext will contain .js or .css, because .mjs recognizes as .js
|
|
423
|
+
const ext = extMatch[1] === 'mjs' ? 'js' : extMatch[1];
|
|
424
|
+
|
|
425
|
+
assets[ext].push(entryPointPublicPath);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return assets;
|
|
430
|
+
}
|
|
431
|
+
|
|
110
432
|
/**
|
|
111
433
|
* Once webpack is done with compiling the template into a NodeJS code this function
|
|
112
434
|
* evaluates it to generate the html result
|
|
@@ -115,6 +437,7 @@ class HtmlWebpackPlugin {
|
|
|
115
437
|
* Please change that in a further refactoring
|
|
116
438
|
*
|
|
117
439
|
* @param {string} source
|
|
440
|
+
* @param {string} publicPath
|
|
118
441
|
* @param {string} templateFilename
|
|
119
442
|
* @returns {Promise<string | (() => string | Promise<string>)>}
|
|
120
443
|
*/
|
|
@@ -122,11 +445,13 @@ class HtmlWebpackPlugin {
|
|
|
122
445
|
if (!source) {
|
|
123
446
|
return Promise.reject(new Error('The child compilation didn\'t provide a result'));
|
|
124
447
|
}
|
|
448
|
+
|
|
125
449
|
// The LibraryTemplatePlugin stores the template result in a local variable.
|
|
126
450
|
// By adding it to the end the value gets extracted during evaluation
|
|
127
451
|
if (source.indexOf('HTML_WEBPACK_PLUGIN_RESULT') >= 0) {
|
|
128
452
|
source += ';\nHTML_WEBPACK_PLUGIN_RESULT';
|
|
129
453
|
}
|
|
454
|
+
|
|
130
455
|
const templateWithoutLoaders = templateFilename.replace(/^.+!/, '').replace(/\?.+$/, '');
|
|
131
456
|
const vmContext = vm.createContext({
|
|
132
457
|
...global,
|
|
@@ -184,314 +509,102 @@ class HtmlWebpackPlugin {
|
|
|
184
509
|
WritableStreamDefaultController: global.WritableStreamDefaultController,
|
|
185
510
|
WritableStreamDefaultWriter: global.WritableStreamDefaultWriter
|
|
186
511
|
});
|
|
512
|
+
|
|
187
513
|
const vmScript = new vm.Script(source, { filename: templateWithoutLoaders });
|
|
514
|
+
|
|
188
515
|
// Evaluate code and cast to string
|
|
189
516
|
let newSource;
|
|
517
|
+
|
|
190
518
|
try {
|
|
191
519
|
newSource = vmScript.runInContext(vmContext);
|
|
192
520
|
} catch (e) {
|
|
193
521
|
return Promise.reject(e);
|
|
194
522
|
}
|
|
523
|
+
|
|
195
524
|
if (typeof newSource === 'object' && newSource.__esModule && newSource.default) {
|
|
196
525
|
newSource = newSource.default;
|
|
197
526
|
}
|
|
527
|
+
|
|
198
528
|
return typeof newSource === 'string' || typeof newSource === 'function'
|
|
199
529
|
? Promise.resolve(newSource)
|
|
200
530
|
: Promise.reject(new Error('The loader "' + templateWithoutLoaders + '" didn\'t return html.'));
|
|
201
531
|
}
|
|
202
|
-
}
|
|
203
532
|
|
|
204
|
-
/**
|
|
205
|
-
* connect the html-webpack-plugin to the webpack compiler lifecycle hooks
|
|
206
|
-
*
|
|
207
|
-
* @param {import('webpack').Compiler} compiler
|
|
208
|
-
* @param {ProcessedHtmlWebpackOptions} options
|
|
209
|
-
* @param {HtmlWebpackPlugin} plugin
|
|
210
|
-
*/
|
|
211
|
-
function hookIntoCompiler (compiler, options, plugin) {
|
|
212
|
-
const webpack = compiler.webpack;
|
|
213
|
-
// Instance variables to keep caching information
|
|
214
|
-
// for multiple builds
|
|
215
|
-
let assetJson;
|
|
216
533
|
/**
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* @
|
|
534
|
+
* Add toString methods for easier rendering inside the template
|
|
535
|
+
*
|
|
536
|
+
* @private
|
|
537
|
+
* @param {Array<HtmlTagObject>} assetTagGroup
|
|
538
|
+
* @returns {Array<HtmlTagObject>}
|
|
220
539
|
*/
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
// convert absolute filename into relative so that webpack can
|
|
232
|
-
// generate it at correct location
|
|
233
|
-
const filename = options.filename;
|
|
234
|
-
if (path.resolve(filename) === path.normalize(filename)) {
|
|
235
|
-
const outputPath = /** @type {string} - Once initialized the path is always a string */(compiler.options.output.path);
|
|
236
|
-
options.filename = path.relative(outputPath, filename);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Check if webpack is running in production mode
|
|
240
|
-
// @see https://github.com/webpack/webpack/blob/3366421f1784c449f415cda5930a8e445086f688/lib/WebpackOptionsDefaulter.js#L12-L14
|
|
241
|
-
const isProductionLikeMode = compiler.options.mode === 'production' || !compiler.options.mode;
|
|
242
|
-
|
|
243
|
-
const minify = options.minify;
|
|
244
|
-
if (minify === true || (minify === 'auto' && isProductionLikeMode)) {
|
|
245
|
-
/** @type { import('html-minifier-terser').Options } */
|
|
246
|
-
options.minify = {
|
|
247
|
-
// https://www.npmjs.com/package/html-minifier-terser#options-quick-reference
|
|
248
|
-
collapseWhitespace: true,
|
|
249
|
-
keepClosingSlash: true,
|
|
250
|
-
removeComments: true,
|
|
251
|
-
removeRedundantAttributes: true,
|
|
252
|
-
removeScriptTypeAttributes: true,
|
|
253
|
-
removeStyleLinkTypeAttributes: true,
|
|
254
|
-
useShortDoctype: true
|
|
255
|
-
};
|
|
540
|
+
prepareAssetTagGroupForRendering (assetTagGroup) {
|
|
541
|
+
const xhtml = this.options.xhtml;
|
|
542
|
+
return HtmlTagArray.from(assetTagGroup.map((assetTag) => {
|
|
543
|
+
const copiedAssetTag = Object.assign({}, assetTag);
|
|
544
|
+
copiedAssetTag.toString = function () {
|
|
545
|
+
return htmlTagObjectToString(this, xhtml);
|
|
546
|
+
};
|
|
547
|
+
return copiedAssetTag;
|
|
548
|
+
}));
|
|
256
549
|
}
|
|
257
550
|
|
|
258
|
-
compiler.hooks.thisCompilation.tap('HtmlWebpackPlugin',
|
|
259
|
-
/**
|
|
260
|
-
* Hook into the webpack compilation
|
|
261
|
-
* @param {WebpackCompilation} compilation
|
|
262
|
-
*/
|
|
263
|
-
(compilation) => {
|
|
264
|
-
compilation.hooks.processAssets.tapAsync(
|
|
265
|
-
{
|
|
266
|
-
name: 'HtmlWebpackPlugin',
|
|
267
|
-
stage:
|
|
268
|
-
/**
|
|
269
|
-
* Generate the html after minification and dev tooling is done
|
|
270
|
-
*/
|
|
271
|
-
webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE
|
|
272
|
-
},
|
|
273
|
-
/**
|
|
274
|
-
* Hook into the process assets hook
|
|
275
|
-
* @param {WebpackCompilation} compilationAssets
|
|
276
|
-
* @param {(err?: Error) => void} callback
|
|
277
|
-
*/
|
|
278
|
-
(compilationAssets, callback) => {
|
|
279
|
-
// Get all entry point names for this html file
|
|
280
|
-
const entryNames = Array.from(compilation.entrypoints.keys());
|
|
281
|
-
const filteredEntryNames = filterChunks(entryNames, options.chunks, options.excludeChunks);
|
|
282
|
-
const sortedEntryNames = sortEntryChunks(filteredEntryNames, options.chunksSortMode, compilation);
|
|
283
|
-
|
|
284
|
-
const templateResult = options.templateContent
|
|
285
|
-
? { mainCompilationHash: compilation.hash }
|
|
286
|
-
: childCompilerPlugin.getCompilationEntryResult(options.template);
|
|
287
|
-
|
|
288
|
-
if ('error' in templateResult) {
|
|
289
|
-
compilation.errors.push(prettyError(templateResult.error, compiler.context).toString());
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// If the child compilation was not executed during a previous main compile run
|
|
293
|
-
// it is a cached result
|
|
294
|
-
const isCompilationCached = templateResult.mainCompilationHash !== compilation.hash;
|
|
295
|
-
|
|
296
|
-
/** The public path used inside the html file */
|
|
297
|
-
const htmlPublicPath = getPublicPath(compilation, options.filename, options.publicPath);
|
|
298
|
-
|
|
299
|
-
/** Generated file paths from the entry point names */
|
|
300
|
-
const assets = htmlWebpackPluginAssets(compilation, sortedEntryNames, htmlPublicPath);
|
|
301
|
-
|
|
302
|
-
// If the template and the assets did not change we don't have to emit the html
|
|
303
|
-
const newAssetJson = JSON.stringify(getAssetFiles(assets));
|
|
304
|
-
if (isCompilationCached && options.cache && assetJson === newAssetJson) {
|
|
305
|
-
previousEmittedAssets.forEach(({ name, html }) => {
|
|
306
|
-
compilation.emitAsset(name, new webpack.sources.RawSource(html, false));
|
|
307
|
-
});
|
|
308
|
-
return callback();
|
|
309
|
-
} else {
|
|
310
|
-
previousEmittedAssets = [];
|
|
311
|
-
assetJson = newAssetJson;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// The html-webpack plugin uses a object representation for the html-tags which will be injected
|
|
315
|
-
// to allow altering them more easily
|
|
316
|
-
// Just before they are converted a third-party-plugin author might change the order and content
|
|
317
|
-
const assetsPromise = getFaviconPublicPath(options.favicon, compilation, assets.publicPath)
|
|
318
|
-
.then((faviconPath) => {
|
|
319
|
-
assets.favicon = faviconPath;
|
|
320
|
-
return getHtmlWebpackPluginHooks(compilation).beforeAssetTagGeneration.promise({
|
|
321
|
-
assets: assets,
|
|
322
|
-
outputName: options.filename,
|
|
323
|
-
plugin: plugin
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
// Turn the js and css paths into grouped HtmlTagObjects
|
|
328
|
-
const assetTagGroupsPromise = assetsPromise
|
|
329
|
-
// And allow third-party-plugin authors to reorder and change the assetTags before they are grouped
|
|
330
|
-
.then(({ assets }) => getHtmlWebpackPluginHooks(compilation).alterAssetTags.promise({
|
|
331
|
-
assetTags: {
|
|
332
|
-
scripts: generatedScriptTags(assets.js),
|
|
333
|
-
styles: generateStyleTags(assets.css),
|
|
334
|
-
meta: [
|
|
335
|
-
...generateBaseTag(options.base),
|
|
336
|
-
...generatedMetaTags(options.meta),
|
|
337
|
-
...generateFaviconTags(assets.favicon)
|
|
338
|
-
]
|
|
339
|
-
},
|
|
340
|
-
outputName: options.filename,
|
|
341
|
-
publicPath: htmlPublicPath,
|
|
342
|
-
plugin: plugin
|
|
343
|
-
}))
|
|
344
|
-
.then(({ assetTags }) => {
|
|
345
|
-
// Inject scripts to body unless it set explicitly to head
|
|
346
|
-
const scriptTarget = options.inject === 'head' ||
|
|
347
|
-
(options.inject !== 'body' && options.scriptLoading !== 'blocking') ? 'head' : 'body';
|
|
348
|
-
// Group assets to `head` and `body` tag arrays
|
|
349
|
-
const assetGroups = generateAssetGroups(assetTags, scriptTarget);
|
|
350
|
-
// Allow third-party-plugin authors to reorder and change the assetTags once they are grouped
|
|
351
|
-
return getHtmlWebpackPluginHooks(compilation).alterAssetTagGroups.promise({
|
|
352
|
-
headTags: assetGroups.headTags,
|
|
353
|
-
bodyTags: assetGroups.bodyTags,
|
|
354
|
-
outputName: options.filename,
|
|
355
|
-
publicPath: htmlPublicPath,
|
|
356
|
-
plugin: plugin
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
// Turn the compiled template into a nodejs function or into a nodejs string
|
|
361
|
-
const templateEvaluationPromise = Promise.resolve()
|
|
362
|
-
.then(() => {
|
|
363
|
-
if ('error' in templateResult) {
|
|
364
|
-
return options.showErrors ? prettyError(templateResult.error, compiler.context).toHtml() : 'ERROR';
|
|
365
|
-
}
|
|
366
|
-
// Allow to use a custom function / string instead
|
|
367
|
-
if (options.templateContent !== false) {
|
|
368
|
-
return options.templateContent;
|
|
369
|
-
}
|
|
370
|
-
// Once everything is compiled evaluate the html factory
|
|
371
|
-
// and replace it with its content
|
|
372
|
-
return ('compiledEntry' in templateResult)
|
|
373
|
-
? plugin.evaluateCompilationResult(templateResult.compiledEntry.content, htmlPublicPath, options.template)
|
|
374
|
-
: Promise.reject(new Error('Child compilation contained no compiledEntry'));
|
|
375
|
-
});
|
|
376
|
-
const templateExectutionPromise = Promise.all([assetsPromise, assetTagGroupsPromise, templateEvaluationPromise])
|
|
377
|
-
// Execute the template
|
|
378
|
-
.then(([assetsHookResult, assetTags, compilationResult]) => typeof compilationResult !== 'function'
|
|
379
|
-
? compilationResult
|
|
380
|
-
: executeTemplate(compilationResult, assetsHookResult.assets, { headTags: assetTags.headTags, bodyTags: assetTags.bodyTags }, compilation));
|
|
381
|
-
|
|
382
|
-
const injectedHtmlPromise = Promise.all([assetTagGroupsPromise, templateExectutionPromise])
|
|
383
|
-
// Allow plugins to change the html before assets are injected
|
|
384
|
-
.then(([assetTags, html]) => {
|
|
385
|
-
const pluginArgs = { html, headTags: assetTags.headTags, bodyTags: assetTags.bodyTags, plugin: plugin, outputName: options.filename };
|
|
386
|
-
return getHtmlWebpackPluginHooks(compilation).afterTemplateExecution.promise(pluginArgs);
|
|
387
|
-
})
|
|
388
|
-
.then(({ html, headTags, bodyTags }) => {
|
|
389
|
-
return postProcessHtml(html, assets, { headTags, bodyTags });
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
const emitHtmlPromise = injectedHtmlPromise
|
|
393
|
-
// Allow plugins to change the html after assets are injected
|
|
394
|
-
.then((html) => {
|
|
395
|
-
const pluginArgs = { html, plugin: plugin, outputName: options.filename };
|
|
396
|
-
return getHtmlWebpackPluginHooks(compilation).beforeEmit.promise(pluginArgs)
|
|
397
|
-
.then(result => result.html);
|
|
398
|
-
})
|
|
399
|
-
.catch(err => {
|
|
400
|
-
// In case anything went wrong the promise is resolved
|
|
401
|
-
// with the error message and an error is logged
|
|
402
|
-
compilation.errors.push(prettyError(err, compiler.context).toString());
|
|
403
|
-
return options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
|
|
404
|
-
})
|
|
405
|
-
.then(html => {
|
|
406
|
-
const filename = options.filename.replace(/\[templatehash([^\]]*)\]/g, require('util').deprecate(
|
|
407
|
-
(match, options) => `[contenthash${options}]`,
|
|
408
|
-
'[templatehash] is now [contenthash]')
|
|
409
|
-
);
|
|
410
|
-
const replacedFilename = replacePlaceholdersInFilename(filename, html, compilation);
|
|
411
|
-
// Add the evaluated html code to the webpack assets
|
|
412
|
-
compilation.emitAsset(replacedFilename.path, new webpack.sources.RawSource(html, false), replacedFilename.info);
|
|
413
|
-
previousEmittedAssets.push({ name: replacedFilename.path, html });
|
|
414
|
-
return replacedFilename.path;
|
|
415
|
-
})
|
|
416
|
-
.then((finalOutputName) => getHtmlWebpackPluginHooks(compilation).afterEmit.promise({
|
|
417
|
-
outputName: finalOutputName,
|
|
418
|
-
plugin: plugin
|
|
419
|
-
}).catch(err => {
|
|
420
|
-
console.error(err);
|
|
421
|
-
return null;
|
|
422
|
-
}).then(() => null));
|
|
423
|
-
|
|
424
|
-
// Once all files are added to the webpack compilation
|
|
425
|
-
// let the webpack compiler continue
|
|
426
|
-
emitHtmlPromise.then(() => {
|
|
427
|
-
callback();
|
|
428
|
-
});
|
|
429
|
-
});
|
|
430
|
-
});
|
|
431
|
-
|
|
432
551
|
/**
|
|
433
552
|
* Generate the template parameters for the template function
|
|
434
|
-
*
|
|
435
|
-
* @
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
css: Array<string>,
|
|
439
|
-
manifest?: string,
|
|
440
|
-
favicon?: string
|
|
441
|
-
}} assets
|
|
553
|
+
*
|
|
554
|
+
* @private
|
|
555
|
+
* @param {Compilation} compilation
|
|
556
|
+
* @param {AssetsInformationByGroups} assetsInformationByGroups
|
|
442
557
|
* @param {{
|
|
443
558
|
headTags: HtmlTagObject[],
|
|
444
559
|
bodyTags: HtmlTagObject[]
|
|
445
560
|
}} assetTags
|
|
446
561
|
* @returns {Promise<{[key: any]: any}>}
|
|
447
562
|
*/
|
|
448
|
-
|
|
449
|
-
const templateParameters = options.templateParameters;
|
|
563
|
+
getTemplateParameters (compilation, assetsInformationByGroups, assetTags) {
|
|
564
|
+
const templateParameters = this.options.templateParameters;
|
|
565
|
+
|
|
450
566
|
if (templateParameters === false) {
|
|
451
567
|
return Promise.resolve({});
|
|
452
568
|
}
|
|
569
|
+
|
|
453
570
|
if (typeof templateParameters !== 'function' && typeof templateParameters !== 'object') {
|
|
454
571
|
throw new Error('templateParameters has to be either a function or an object');
|
|
455
572
|
}
|
|
573
|
+
|
|
456
574
|
const templateParameterFunction = typeof templateParameters === 'function'
|
|
457
575
|
// A custom function can overwrite the entire template parameter preparation
|
|
458
576
|
? templateParameters
|
|
459
577
|
// If the template parameters is an object merge it with the default values
|
|
460
|
-
: (compilation,
|
|
461
|
-
templateParametersGenerator(compilation,
|
|
578
|
+
: (compilation, assetsInformationByGroups, assetTags, options) => Object.assign({},
|
|
579
|
+
templateParametersGenerator(compilation, assetsInformationByGroups, assetTags, options),
|
|
462
580
|
templateParameters
|
|
463
581
|
);
|
|
464
582
|
const preparedAssetTags = {
|
|
465
|
-
headTags: prepareAssetTagGroupForRendering(assetTags.headTags),
|
|
466
|
-
bodyTags: prepareAssetTagGroupForRendering(assetTags.bodyTags)
|
|
583
|
+
headTags: this.prepareAssetTagGroupForRendering(assetTags.headTags),
|
|
584
|
+
bodyTags: this.prepareAssetTagGroupForRendering(assetTags.bodyTags)
|
|
467
585
|
};
|
|
468
586
|
return Promise
|
|
469
587
|
.resolve()
|
|
470
|
-
.then(() => templateParameterFunction(compilation,
|
|
588
|
+
.then(() => templateParameterFunction(compilation, assetsInformationByGroups, preparedAssetTags, this.options));
|
|
471
589
|
}
|
|
472
590
|
|
|
473
591
|
/**
|
|
474
592
|
* This function renders the actual html by executing the template function
|
|
475
593
|
*
|
|
594
|
+
* @private
|
|
476
595
|
* @param {(templateParameters) => string | Promise<string>} templateFunction
|
|
477
|
-
* @param {
|
|
478
|
-
publicPath: string,
|
|
479
|
-
js: Array<string>,
|
|
480
|
-
css: Array<string>,
|
|
481
|
-
manifest?: string,
|
|
482
|
-
favicon?: string
|
|
483
|
-
}} assets
|
|
596
|
+
* @param {AssetsInformationByGroups} assetsInformationByGroups
|
|
484
597
|
* @param {{
|
|
485
598
|
headTags: HtmlTagObject[],
|
|
486
599
|
bodyTags: HtmlTagObject[]
|
|
487
600
|
}} assetTags
|
|
488
|
-
* @param {
|
|
489
|
-
*
|
|
601
|
+
* @param {Compilation} compilation
|
|
490
602
|
* @returns Promise<string>
|
|
491
603
|
*/
|
|
492
|
-
|
|
604
|
+
executeTemplate (templateFunction, assetsInformationByGroups, assetTags, compilation) {
|
|
493
605
|
// Template processing
|
|
494
|
-
const templateParamsPromise = getTemplateParameters(compilation,
|
|
606
|
+
const templateParamsPromise = this.getTemplateParameters(compilation, assetsInformationByGroups, assetTags);
|
|
607
|
+
|
|
495
608
|
return templateParamsPromise.then((templateParams) => {
|
|
496
609
|
try {
|
|
497
610
|
// If html is a promise return the promise
|
|
@@ -507,299 +620,203 @@ function hookIntoCompiler (compiler, options, plugin) {
|
|
|
507
620
|
/**
|
|
508
621
|
* Html Post processing
|
|
509
622
|
*
|
|
510
|
-
* @
|
|
511
|
-
* The
|
|
512
|
-
* @param {any}
|
|
513
|
-
* @param {
|
|
514
|
-
|
|
515
|
-
bodyTags: HtmlTagObject[]
|
|
516
|
-
}} assetTags
|
|
517
|
-
* The asset tags to inject
|
|
518
|
-
*
|
|
623
|
+
* @private
|
|
624
|
+
* @param {Compiler} compiler The compiler instance
|
|
625
|
+
* @param {any} originalHtml The input html
|
|
626
|
+
* @param {AssetsInformationByGroups} assetsInformationByGroups
|
|
627
|
+
* @param {{headTags: HtmlTagObject[], bodyTags: HtmlTagObject[]}} assetTags The asset tags to inject
|
|
519
628
|
* @returns {Promise<string>}
|
|
520
629
|
*/
|
|
521
|
-
|
|
630
|
+
postProcessHtml (compiler, originalHtml, assetsInformationByGroups, assetTags) {
|
|
631
|
+
let html = originalHtml;
|
|
632
|
+
|
|
522
633
|
if (typeof html !== 'string') {
|
|
523
634
|
return Promise.reject(new Error('Expected html to be a string but got ' + JSON.stringify(html)));
|
|
524
635
|
}
|
|
525
|
-
const htmlAfterInjection = options.inject
|
|
526
|
-
? injectAssetsIntoHtml(html, assets, assetTags)
|
|
527
|
-
: html;
|
|
528
|
-
const htmlAfterMinification = minifyHtml(htmlAfterInjection);
|
|
529
|
-
return Promise.resolve(htmlAfterMinification);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/*
|
|
533
|
-
* Pushes the content of the given filename to the compilation assets
|
|
534
|
-
* @param {string} filename
|
|
535
|
-
* @param {WebpackCompilation} compilation
|
|
536
|
-
*
|
|
537
|
-
* @returns {string} file basename
|
|
538
|
-
*/
|
|
539
|
-
function addFileToAssets (filename, compilation) {
|
|
540
|
-
filename = path.resolve(compilation.compiler.context, filename);
|
|
541
|
-
return fsReadFileAsync(filename)
|
|
542
|
-
.then(source => new webpack.sources.RawSource(source, false))
|
|
543
|
-
.catch(() => Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename)))
|
|
544
|
-
.then(rawSource => {
|
|
545
|
-
const basename = path.basename(filename);
|
|
546
|
-
compilation.fileDependencies.add(filename);
|
|
547
|
-
compilation.emitAsset(basename, rawSource);
|
|
548
|
-
return basename;
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
636
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
function replacePlaceholdersInFilename (filename, fileContent, compilation) {
|
|
563
|
-
if (/\[\\*([\w:]+)\\*\]/i.test(filename) === false) {
|
|
564
|
-
return { path: filename, info: {} };
|
|
565
|
-
}
|
|
566
|
-
const hash = compiler.webpack.util.createHash(compilation.outputOptions.hashFunction);
|
|
567
|
-
hash.update(fileContent);
|
|
568
|
-
if (compilation.outputOptions.hashSalt) {
|
|
569
|
-
hash.update(compilation.outputOptions.hashSalt);
|
|
570
|
-
}
|
|
571
|
-
const contentHash = hash.digest(compilation.outputOptions.hashDigest).slice(0, compilation.outputOptions.hashDigestLength);
|
|
572
|
-
return compilation.getPathWithInfo(
|
|
573
|
-
filename,
|
|
574
|
-
{
|
|
575
|
-
contentHash,
|
|
576
|
-
chunk: {
|
|
577
|
-
hash: contentHash,
|
|
578
|
-
contentHash
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* Helper to sort chunks
|
|
586
|
-
* @param {string[]} entryNames
|
|
587
|
-
* @param {string|((entryNameA: string, entryNameB: string) => number)} sortMode
|
|
588
|
-
* @param {WebpackCompilation} compilation
|
|
589
|
-
*/
|
|
590
|
-
function sortEntryChunks (entryNames, sortMode, compilation) {
|
|
591
|
-
// Custom function
|
|
592
|
-
if (typeof sortMode === 'function') {
|
|
593
|
-
return entryNames.sort(sortMode);
|
|
594
|
-
}
|
|
595
|
-
// Check if the given sort mode is a valid chunkSorter sort mode
|
|
596
|
-
if (typeof chunkSorter[sortMode] !== 'undefined') {
|
|
597
|
-
return chunkSorter[sortMode](entryNames, compilation, options);
|
|
598
|
-
}
|
|
599
|
-
throw new Error('"' + sortMode + '" is not a valid chunk sort mode');
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/**
|
|
603
|
-
* Return all chunks from the compilation result which match the exclude and include filters
|
|
604
|
-
* @param {any} chunks
|
|
605
|
-
* @param {string[]|'all'} includedChunks
|
|
606
|
-
* @param {string[]} excludedChunks
|
|
607
|
-
*/
|
|
608
|
-
function filterChunks (chunks, includedChunks, excludedChunks) {
|
|
609
|
-
return chunks.filter(chunkName => {
|
|
610
|
-
// Skip if the chunks should be filtered and the given chunk was not added explicity
|
|
611
|
-
if (Array.isArray(includedChunks) && includedChunks.indexOf(chunkName) === -1) {
|
|
612
|
-
return false;
|
|
613
|
-
}
|
|
614
|
-
// Skip if the chunks should be filtered and the given chunk was excluded explicity
|
|
615
|
-
if (Array.isArray(excludedChunks) && excludedChunks.indexOf(chunkName) !== -1) {
|
|
616
|
-
return false;
|
|
617
|
-
}
|
|
618
|
-
// Add otherwise
|
|
619
|
-
return true;
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* Generate the relative or absolute base url to reference images, css, and javascript files
|
|
625
|
-
* from within the html file - the publicPath
|
|
626
|
-
*
|
|
627
|
-
* @param {WebpackCompilation} compilation
|
|
628
|
-
* @param {string} childCompilationOutputName
|
|
629
|
-
* @param {string | 'auto'} customPublicPath
|
|
630
|
-
* @returns {string}
|
|
631
|
-
*/
|
|
632
|
-
function getPublicPath (compilation, childCompilationOutputName, customPublicPath) {
|
|
633
|
-
const compilationHash = compilation.hash;
|
|
634
|
-
|
|
635
|
-
/**
|
|
636
|
-
* @type {string} the configured public path to the asset root
|
|
637
|
-
* if a path publicPath is set in the current webpack config use it otherwise
|
|
638
|
-
* fallback to a relative path
|
|
639
|
-
*/
|
|
640
|
-
const webpackPublicPath = compilation.getAssetPath(compilation.outputOptions.publicPath, { hash: compilationHash });
|
|
641
|
-
|
|
642
|
-
// Webpack 5 introduced "auto" as default value
|
|
643
|
-
const isPublicPathDefined = webpackPublicPath !== 'auto';
|
|
644
|
-
|
|
645
|
-
let publicPath =
|
|
646
|
-
// If the html-webpack-plugin options contain a custom public path uset it
|
|
647
|
-
customPublicPath !== 'auto'
|
|
648
|
-
? customPublicPath
|
|
649
|
-
: (isPublicPathDefined
|
|
650
|
-
// If a hard coded public path exists use it
|
|
651
|
-
? webpackPublicPath
|
|
652
|
-
// If no public path was set get a relative url path
|
|
653
|
-
: path.relative(path.resolve(compilation.options.output.path, path.dirname(childCompilationOutputName)), compilation.options.output.path)
|
|
654
|
-
.split(path.sep).join('/')
|
|
655
|
-
);
|
|
656
|
-
|
|
657
|
-
if (publicPath.length && publicPath.substr(-1, 1) !== '/') {
|
|
658
|
-
publicPath += '/';
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
return publicPath;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/**
|
|
665
|
-
* The htmlWebpackPluginAssets extracts the asset information of a webpack compilation
|
|
666
|
-
* for all given entry names
|
|
667
|
-
* @param {WebpackCompilation} compilation
|
|
668
|
-
* @param {string[]} entryNames
|
|
669
|
-
* @param {string | 'auto'} publicPath
|
|
670
|
-
* @returns {{
|
|
671
|
-
publicPath: string,
|
|
672
|
-
js: Array<string>,
|
|
673
|
-
css: Array<string>,
|
|
674
|
-
manifest?: string,
|
|
675
|
-
favicon?: string
|
|
676
|
-
}}
|
|
677
|
-
*/
|
|
678
|
-
function htmlWebpackPluginAssets (compilation, entryNames, publicPath) {
|
|
679
|
-
const compilationHash = compilation.hash;
|
|
680
|
-
/**
|
|
681
|
-
* @type {{
|
|
682
|
-
publicPath: string,
|
|
683
|
-
js: Array<string>,
|
|
684
|
-
css: Array<string>,
|
|
685
|
-
manifest?: string,
|
|
686
|
-
favicon?: string
|
|
687
|
-
}}
|
|
688
|
-
*/
|
|
689
|
-
const assets = {
|
|
690
|
-
// The public path
|
|
691
|
-
publicPath,
|
|
692
|
-
// Will contain all js and mjs files
|
|
693
|
-
js: [],
|
|
694
|
-
// Will contain all css files
|
|
695
|
-
css: [],
|
|
696
|
-
// Will contain the html5 appcache manifest files if it exists
|
|
697
|
-
manifest: Object.keys(compilation.assets).find(assetFile => path.extname(assetFile) === '.appcache'),
|
|
698
|
-
// Favicon
|
|
699
|
-
favicon: undefined
|
|
700
|
-
};
|
|
701
|
-
|
|
702
|
-
// Append a hash for cache busting
|
|
703
|
-
if (options.hash && assets.manifest) {
|
|
704
|
-
assets.manifest = appendHash(assets.manifest, compilationHash);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
// Extract paths to .js, .mjs and .css files from the current compilation
|
|
708
|
-
const entryPointPublicPathMap = {};
|
|
709
|
-
const extensionRegexp = /\.(css|js|mjs)(\?|$)/;
|
|
710
|
-
for (let i = 0; i < entryNames.length; i++) {
|
|
711
|
-
const entryName = entryNames[i];
|
|
712
|
-
/** entryPointUnfilteredFiles - also includes hot module update files */
|
|
713
|
-
const entryPointUnfilteredFiles = compilation.entrypoints.get(entryName).getFiles();
|
|
714
|
-
|
|
715
|
-
const entryPointFiles = entryPointUnfilteredFiles.filter((chunkFile) => {
|
|
716
|
-
const asset = compilation.getAsset(chunkFile);
|
|
717
|
-
if (!asset) {
|
|
718
|
-
return true;
|
|
719
|
-
}
|
|
720
|
-
// Prevent hot-module files from being included:
|
|
721
|
-
const assetMetaInformation = asset.info || {};
|
|
722
|
-
return !(assetMetaInformation.hotModuleReplacement || assetMetaInformation.development);
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
// Prepend the publicPath and append the hash depending on the
|
|
726
|
-
// webpack.output.publicPath and hashOptions
|
|
727
|
-
// E.g. bundle.js -> /bundle.js?hash
|
|
728
|
-
const entryPointPublicPaths = entryPointFiles
|
|
729
|
-
.map(chunkFile => {
|
|
730
|
-
const entryPointPublicPath = publicPath + urlencodePath(chunkFile);
|
|
731
|
-
return options.hash
|
|
732
|
-
? appendHash(entryPointPublicPath, compilationHash)
|
|
733
|
-
: entryPointPublicPath;
|
|
734
|
-
});
|
|
637
|
+
if (this.options.inject) {
|
|
638
|
+
const htmlRegExp = /(<html[^>]*>)/i;
|
|
639
|
+
const headRegExp = /(<\/head\s*>)/i;
|
|
640
|
+
const bodyRegExp = /(<\/body\s*>)/i;
|
|
641
|
+
const metaViewportRegExp = /<meta[^>]+name=["']viewport["'][^>]*>/i;
|
|
642
|
+
const body = assetTags.bodyTags.map((assetTagObject) => htmlTagObjectToString(assetTagObject, this.options.xhtml));
|
|
643
|
+
const head = assetTags.headTags.filter((item) => {
|
|
644
|
+
if (item.tagName === 'meta' && item.attributes && item.attributes.name === 'viewport' && metaViewportRegExp.test(html)) {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
735
647
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
648
|
+
return true;
|
|
649
|
+
}).map((assetTagObject) => htmlTagObjectToString(assetTagObject, this.options.xhtml));
|
|
650
|
+
|
|
651
|
+
if (body.length) {
|
|
652
|
+
if (bodyRegExp.test(html)) {
|
|
653
|
+
// Append assets to body element
|
|
654
|
+
html = html.replace(bodyRegExp, match => body.join('') + match);
|
|
655
|
+
} else {
|
|
656
|
+
// Append scripts to the end of the file if no <body> element exists:
|
|
657
|
+
html += body.join('');
|
|
741
658
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (head.length) {
|
|
662
|
+
// Create a head tag if none exists
|
|
663
|
+
if (!headRegExp.test(html)) {
|
|
664
|
+
if (!htmlRegExp.test(html)) {
|
|
665
|
+
html = '<head></head>' + html;
|
|
666
|
+
} else {
|
|
667
|
+
html = html.replace(htmlRegExp, match => match + '<head></head>');
|
|
668
|
+
}
|
|
746
669
|
}
|
|
747
|
-
|
|
748
|
-
//
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
670
|
+
|
|
671
|
+
// Append assets to head element
|
|
672
|
+
html = html.replace(headRegExp, match => head.join('') + match);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Inject manifest into the opening html tag
|
|
676
|
+
if (assetsInformationByGroups.manifest) {
|
|
677
|
+
html = html.replace(/(<html[^>]*)(>)/i, (match, start, end) => {
|
|
678
|
+
// Append the manifest only if no manifest was specified
|
|
679
|
+
if (/\smanifest\s*=/.test(match)) {
|
|
680
|
+
return match;
|
|
681
|
+
}
|
|
682
|
+
return start + ' manifest="' + assetsInformationByGroups.manifest + '"' + end;
|
|
683
|
+
});
|
|
684
|
+
}
|
|
752
685
|
}
|
|
753
|
-
|
|
686
|
+
|
|
687
|
+
// TODO avoid this logic and use https://github.com/webpack-contrib/html-minimizer-webpack-plugin under the hood in the next major version
|
|
688
|
+
// Check if webpack is running in production mode
|
|
689
|
+
// @see https://github.com/webpack/webpack/blob/3366421f1784c449f415cda5930a8e445086f688/lib/WebpackOptionsDefaulter.js#L12-L14
|
|
690
|
+
const isProductionLikeMode = compiler.options.mode === 'production' || !compiler.options.mode;
|
|
691
|
+
const needMinify = this.options.minify === true || typeof this.options.minify === 'object' || (this.options.minify === 'auto' && isProductionLikeMode);
|
|
692
|
+
|
|
693
|
+
if (!needMinify) {
|
|
694
|
+
return Promise.resolve(html);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const minifyOptions = typeof this.options.minify === 'object'
|
|
698
|
+
? this.options.minify
|
|
699
|
+
: {
|
|
700
|
+
// https://www.npmjs.com/package/html-minifier-terser#options-quick-reference
|
|
701
|
+
collapseWhitespace: true,
|
|
702
|
+
keepClosingSlash: true,
|
|
703
|
+
removeComments: true,
|
|
704
|
+
removeRedundantAttributes: true,
|
|
705
|
+
removeScriptTypeAttributes: true,
|
|
706
|
+
removeStyleLinkTypeAttributes: true,
|
|
707
|
+
useShortDoctype: true
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
try {
|
|
711
|
+
html = require('html-minifier-terser').minify(html, minifyOptions);
|
|
712
|
+
} catch (e) {
|
|
713
|
+
const isParseError = String(e.message).indexOf('Parse Error') === 0;
|
|
714
|
+
|
|
715
|
+
if (isParseError) {
|
|
716
|
+
e.message = 'html-webpack-plugin could not minify the generated output.\n' +
|
|
717
|
+
'In production mode the html minifcation is enabled by default.\n' +
|
|
718
|
+
'If you are not generating a valid html output please disable it manually.\n' +
|
|
719
|
+
'You can do so by adding the following setting to your HtmlWebpackPlugin config:\n|\n|' +
|
|
720
|
+
' minify: false\n|\n' +
|
|
721
|
+
'See https://github.com/jantimon/html-webpack-plugin#options for details.\n\n' +
|
|
722
|
+
'For parser dedicated bugs please create an issue here:\n' +
|
|
723
|
+
'https://danielruf.github.io/html-minifier-terser/' +
|
|
724
|
+
'\n' + e.message;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return Promise.reject(e);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return Promise.resolve(html);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Helper to return a sorted unique array of all asset files out of the asset object
|
|
735
|
+
* @private
|
|
736
|
+
*/
|
|
737
|
+
getAssetFiles (assets) {
|
|
738
|
+
const files = _.uniq(Object.keys(assets).filter(assetType => assetType !== 'chunks' && assets[assetType]).reduce((files, assetType) => files.concat(assets[assetType]), []));
|
|
739
|
+
files.sort();
|
|
740
|
+
return files;
|
|
754
741
|
}
|
|
755
742
|
|
|
756
743
|
/**
|
|
757
|
-
* Converts a favicon file from disk to a webpack resource
|
|
758
|
-
* and returns the url to the resource
|
|
744
|
+
* Converts a favicon file from disk to a webpack resource and returns the url to the resource
|
|
759
745
|
*
|
|
760
|
-
* @
|
|
761
|
-
* @param {
|
|
746
|
+
* @private
|
|
747
|
+
* @param {Compiler} compiler
|
|
748
|
+
* @param {string|false} favicon
|
|
749
|
+
* @param {Compilation} compilation
|
|
762
750
|
* @param {string} publicPath
|
|
751
|
+
* @param {PreviousEmittedAssets} previousEmittedAssets
|
|
763
752
|
* @returns {Promise<string|undefined>}
|
|
764
753
|
*/
|
|
765
|
-
|
|
766
|
-
if (!
|
|
754
|
+
generateFavicon (compiler, favicon, compilation, publicPath, previousEmittedAssets) {
|
|
755
|
+
if (!favicon) {
|
|
767
756
|
return Promise.resolve(undefined);
|
|
768
757
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
758
|
+
|
|
759
|
+
const filename = path.resolve(compilation.compiler.context, favicon);
|
|
760
|
+
|
|
761
|
+
return promisify(compilation.inputFileSystem.readFile)(filename)
|
|
762
|
+
.then((buf) => {
|
|
763
|
+
const source = new compiler.webpack.sources.RawSource(/** @type {string | Buffer} */ (buf), false);
|
|
764
|
+
const name = path.basename(filename);
|
|
765
|
+
|
|
766
|
+
compilation.fileDependencies.add(filename);
|
|
767
|
+
compilation.emitAsset(name, source);
|
|
768
|
+
previousEmittedAssets.push({ name, source });
|
|
769
|
+
|
|
770
|
+
const faviconPath = publicPath + name;
|
|
771
|
+
|
|
772
|
+
if (this.options.hash) {
|
|
773
|
+
return this.appendHash(faviconPath, /** @type {string} */ (compilation.hash));
|
|
774
774
|
}
|
|
775
|
+
|
|
775
776
|
return faviconPath;
|
|
776
|
-
})
|
|
777
|
+
})
|
|
778
|
+
.catch(() => Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename)));
|
|
777
779
|
}
|
|
778
780
|
|
|
779
781
|
/**
|
|
780
782
|
* Generate all tags script for the given file paths
|
|
783
|
+
*
|
|
784
|
+
* @private
|
|
781
785
|
* @param {Array<string>} jsAssets
|
|
782
786
|
* @returns {Array<HtmlTagObject>}
|
|
783
787
|
*/
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
defer
|
|
791
|
-
|
|
792
|
-
|
|
788
|
+
generatedScriptTags (jsAssets) {
|
|
789
|
+
// @ts-ignore
|
|
790
|
+
return jsAssets.map(src => {
|
|
791
|
+
const attributes = {};
|
|
792
|
+
|
|
793
|
+
if (this.options.scriptLoading === 'defer') {
|
|
794
|
+
attributes.defer = true;
|
|
795
|
+
} else if (this.options.scriptLoading === 'module') {
|
|
796
|
+
attributes.type = 'module';
|
|
797
|
+
} else if (this.options.scriptLoading === 'systemjs-module') {
|
|
798
|
+
attributes.type = 'systemjs-module';
|
|
793
799
|
}
|
|
794
|
-
|
|
800
|
+
|
|
801
|
+
attributes.src = src;
|
|
802
|
+
|
|
803
|
+
return {
|
|
804
|
+
tagName: 'script',
|
|
805
|
+
voidTag: false,
|
|
806
|
+
meta: { plugin: 'html-webpack-plugin' },
|
|
807
|
+
attributes
|
|
808
|
+
};
|
|
809
|
+
});
|
|
795
810
|
}
|
|
796
811
|
|
|
797
812
|
/**
|
|
798
813
|
* Generate all style tags for the given file paths
|
|
814
|
+
*
|
|
815
|
+
* @private
|
|
799
816
|
* @param {Array<string>} cssAssets
|
|
800
817
|
* @returns {Array<HtmlTagObject>}
|
|
801
818
|
*/
|
|
802
|
-
|
|
819
|
+
generateStyleTags (cssAssets) {
|
|
803
820
|
return cssAssets.map(styleAsset => ({
|
|
804
821
|
tagName: 'link',
|
|
805
822
|
voidTag: true,
|
|
@@ -813,41 +830,34 @@ function hookIntoCompiler (compiler, options, plugin) {
|
|
|
813
830
|
|
|
814
831
|
/**
|
|
815
832
|
* Generate an optional base tag
|
|
816
|
-
*
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
attributes: (typeof baseOption === 'string') ? {
|
|
831
|
-
href: baseOption
|
|
832
|
-
} : baseOption
|
|
833
|
-
}];
|
|
834
|
-
}
|
|
833
|
+
*
|
|
834
|
+
* @param {string | {[attributeName: string]: string}} base
|
|
835
|
+
* @returns {Array<HtmlTagObject>}
|
|
836
|
+
*/
|
|
837
|
+
generateBaseTag (base) {
|
|
838
|
+
return [{
|
|
839
|
+
tagName: 'base',
|
|
840
|
+
voidTag: true,
|
|
841
|
+
meta: { plugin: 'html-webpack-plugin' },
|
|
842
|
+
// attributes e.g. { href:"http://example.com/page.html" target:"_blank" }
|
|
843
|
+
attributes: typeof base === 'string' ? {
|
|
844
|
+
href: base
|
|
845
|
+
} : base
|
|
846
|
+
}];
|
|
835
847
|
}
|
|
836
848
|
|
|
837
849
|
/**
|
|
838
850
|
* Generate all meta tags for the given meta configuration
|
|
839
|
-
*
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
* @returns {Array<HtmlTagObject>}
|
|
846
|
-
*/
|
|
847
|
-
function generatedMetaTags (metaOptions) {
|
|
851
|
+
*
|
|
852
|
+
* @private
|
|
853
|
+
* @param {false | {[name: string]: false | string | {[attributeName: string]: string|boolean}}} metaOptions
|
|
854
|
+
* @returns {Array<HtmlTagObject>}
|
|
855
|
+
*/
|
|
856
|
+
generatedMetaTags (metaOptions) {
|
|
848
857
|
if (metaOptions === false) {
|
|
849
858
|
return [];
|
|
850
859
|
}
|
|
860
|
+
|
|
851
861
|
// Make tags self-closing in case of xhtml
|
|
852
862
|
// Turn { "viewport" : "width=500, initial-scale=1" } into
|
|
853
863
|
// [{ name:"viewport" content:"width=500, initial-scale=1" }]
|
|
@@ -860,8 +870,9 @@ function hookIntoCompiler (compiler, options, plugin) {
|
|
|
860
870
|
} : metaTagContent;
|
|
861
871
|
})
|
|
862
872
|
.filter((attribute) => attribute !== false);
|
|
863
|
-
|
|
864
|
-
|
|
873
|
+
|
|
874
|
+
// Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
|
|
875
|
+
// the html-webpack-plugin tag structure
|
|
865
876
|
return metaTagAttributeObjects.map((metaTagAttributes) => {
|
|
866
877
|
if (metaTagAttributes === false) {
|
|
867
878
|
throw new Error('Invalid meta tag');
|
|
@@ -877,39 +888,38 @@ function hookIntoCompiler (compiler, options, plugin) {
|
|
|
877
888
|
|
|
878
889
|
/**
|
|
879
890
|
* Generate a favicon tag for the given file path
|
|
880
|
-
*
|
|
891
|
+
*
|
|
892
|
+
* @private
|
|
893
|
+
* @param {string} favicon
|
|
881
894
|
* @returns {Array<HtmlTagObject>}
|
|
882
895
|
*/
|
|
883
|
-
|
|
884
|
-
if (!faviconPath) {
|
|
885
|
-
return [];
|
|
886
|
-
}
|
|
896
|
+
generateFaviconTag (favicon) {
|
|
887
897
|
return [{
|
|
888
898
|
tagName: 'link',
|
|
889
899
|
voidTag: true,
|
|
890
900
|
meta: { plugin: 'html-webpack-plugin' },
|
|
891
901
|
attributes: {
|
|
892
902
|
rel: 'icon',
|
|
893
|
-
href:
|
|
903
|
+
href: favicon
|
|
894
904
|
}
|
|
895
905
|
}];
|
|
896
906
|
}
|
|
897
907
|
|
|
898
908
|
/**
|
|
899
|
-
* Group assets to head and
|
|
909
|
+
* Group assets to head and body tags
|
|
900
910
|
*
|
|
901
911
|
* @param {{
|
|
902
912
|
scripts: Array<HtmlTagObject>;
|
|
903
913
|
styles: Array<HtmlTagObject>;
|
|
904
914
|
meta: Array<HtmlTagObject>;
|
|
905
915
|
}} assetTags
|
|
906
|
-
|
|
907
|
-
|
|
916
|
+
* @param {"body" | "head"} scriptTarget
|
|
917
|
+
* @returns {{
|
|
908
918
|
headTags: Array<HtmlTagObject>;
|
|
909
919
|
bodyTags: Array<HtmlTagObject>;
|
|
910
920
|
}}
|
|
911
|
-
|
|
912
|
-
|
|
921
|
+
*/
|
|
922
|
+
groupAssetsByElements (assetTags, scriptTarget) {
|
|
913
923
|
/** @type {{ headTags: Array<HtmlTagObject>; bodyTags: Array<HtmlTagObject>; }} */
|
|
914
924
|
const result = {
|
|
915
925
|
headTags: [
|
|
@@ -918,214 +928,242 @@ function hookIntoCompiler (compiler, options, plugin) {
|
|
|
918
928
|
],
|
|
919
929
|
bodyTags: []
|
|
920
930
|
};
|
|
931
|
+
|
|
921
932
|
// Add script tags to head or body depending on
|
|
922
933
|
// the htmlPluginOptions
|
|
923
934
|
if (scriptTarget === 'body') {
|
|
924
935
|
result.bodyTags.push(...assetTags.scripts);
|
|
925
936
|
} else {
|
|
926
937
|
// If script loading is blocking add the scripts to the end of the head
|
|
927
|
-
// If script loading is non-blocking add the scripts
|
|
928
|
-
const insertPosition = options.scriptLoading === 'blocking' ? result.headTags.length : assetTags.meta.length;
|
|
938
|
+
// If script loading is non-blocking add the scripts in front of the css files
|
|
939
|
+
const insertPosition = this.options.scriptLoading === 'blocking' ? result.headTags.length : assetTags.meta.length;
|
|
940
|
+
|
|
929
941
|
result.headTags.splice(insertPosition, 0, ...assetTags.scripts);
|
|
930
942
|
}
|
|
931
|
-
return result;
|
|
932
|
-
}
|
|
933
943
|
|
|
934
|
-
|
|
935
|
-
* Add toString methods for easier rendering
|
|
936
|
-
* inside the template
|
|
937
|
-
*
|
|
938
|
-
* @param {Array<HtmlTagObject>} assetTagGroup
|
|
939
|
-
* @returns {Array<HtmlTagObject>}
|
|
940
|
-
*/
|
|
941
|
-
function prepareAssetTagGroupForRendering (assetTagGroup) {
|
|
942
|
-
const xhtml = options.xhtml;
|
|
943
|
-
return HtmlTagArray.from(assetTagGroup.map((assetTag) => {
|
|
944
|
-
const copiedAssetTag = Object.assign({}, assetTag);
|
|
945
|
-
copiedAssetTag.toString = function () {
|
|
946
|
-
return htmlTagObjectToString(this, xhtml);
|
|
947
|
-
};
|
|
948
|
-
return copiedAssetTag;
|
|
949
|
-
}));
|
|
944
|
+
return result;
|
|
950
945
|
}
|
|
951
946
|
|
|
952
947
|
/**
|
|
953
|
-
*
|
|
948
|
+
* Replace [contenthash] in filename
|
|
954
949
|
*
|
|
955
|
-
* @
|
|
956
|
-
* The input html
|
|
957
|
-
* @param {any} assets
|
|
958
|
-
* @param {{
|
|
959
|
-
headTags: HtmlTagObject[],
|
|
960
|
-
bodyTags: HtmlTagObject[]
|
|
961
|
-
}} assetTags
|
|
962
|
-
* The asset tags to inject
|
|
950
|
+
* @see https://survivejs.com/webpack/optimizing/adding-hashes-to-filenames/
|
|
963
951
|
*
|
|
964
|
-
* @
|
|
952
|
+
* @private
|
|
953
|
+
* @param {Compiler} compiler
|
|
954
|
+
* @param {string} filename
|
|
955
|
+
* @param {string|Buffer} fileContent
|
|
956
|
+
* @param {Compilation} compilation
|
|
957
|
+
* @returns {{ path: string, info: {} }}
|
|
965
958
|
*/
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
const bodyRegExp = /(<\/body\s*>)/i;
|
|
970
|
-
const metaViewportRegExp = /<meta[^>]+name=["']viewport["'][^>]*>/i;
|
|
971
|
-
const body = assetTags.bodyTags.map((assetTagObject) => htmlTagObjectToString(assetTagObject, options.xhtml));
|
|
972
|
-
const head = assetTags.headTags.filter((item) => {
|
|
973
|
-
if (item.tagName === 'meta' && item.attributes && item.attributes.name === 'viewport' && metaViewportRegExp.test(html)) {
|
|
974
|
-
return false;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
return true;
|
|
978
|
-
}).map((assetTagObject) => htmlTagObjectToString(assetTagObject, options.xhtml));
|
|
979
|
-
|
|
980
|
-
if (body.length) {
|
|
981
|
-
if (bodyRegExp.test(html)) {
|
|
982
|
-
// Append assets to body element
|
|
983
|
-
html = html.replace(bodyRegExp, match => body.join('') + match);
|
|
984
|
-
} else {
|
|
985
|
-
// Append scripts to the end of the file if no <body> element exists:
|
|
986
|
-
html += body.join('');
|
|
987
|
-
}
|
|
959
|
+
replacePlaceholdersInFilename (compiler, filename, fileContent, compilation) {
|
|
960
|
+
if (/\[\\*([\w:]+)\\*\]/i.test(filename) === false) {
|
|
961
|
+
return { path: filename, info: {} };
|
|
988
962
|
}
|
|
989
963
|
|
|
990
|
-
|
|
991
|
-
// Create a head tag if none exists
|
|
992
|
-
if (!headRegExp.test(html)) {
|
|
993
|
-
if (!htmlRegExp.test(html)) {
|
|
994
|
-
html = '<head></head>' + html;
|
|
995
|
-
} else {
|
|
996
|
-
html = html.replace(htmlRegExp, match => match + '<head></head>');
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
// Append assets to head element
|
|
1001
|
-
html = html.replace(headRegExp, match => head.join('') + match);
|
|
1002
|
-
}
|
|
964
|
+
const hash = compiler.webpack.util.createHash(compilation.outputOptions.hashFunction);
|
|
1003
965
|
|
|
1004
|
-
|
|
1005
|
-
if (assets.manifest) {
|
|
1006
|
-
html = html.replace(/(<html[^>]*)(>)/i, (match, start, end) => {
|
|
1007
|
-
// Append the manifest only if no manifest was specified
|
|
1008
|
-
if (/\smanifest\s*=/.test(match)) {
|
|
1009
|
-
return match;
|
|
1010
|
-
}
|
|
1011
|
-
return start + ' manifest="' + assets.manifest + '"' + end;
|
|
1012
|
-
});
|
|
1013
|
-
}
|
|
1014
|
-
return html;
|
|
1015
|
-
}
|
|
966
|
+
hash.update(fileContent);
|
|
1016
967
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
* E.g. http://localhost:8080/ -> http://localhost:8080/?50c9096ba6183fd728eeb065a26ec175
|
|
1020
|
-
* @param {string} url
|
|
1021
|
-
* @param {string} hash
|
|
1022
|
-
*/
|
|
1023
|
-
function appendHash (url, hash) {
|
|
1024
|
-
if (!url) {
|
|
1025
|
-
return url;
|
|
968
|
+
if (compilation.outputOptions.hashSalt) {
|
|
969
|
+
hash.update(compilation.outputOptions.hashSalt);
|
|
1026
970
|
}
|
|
1027
|
-
return url + (url.indexOf('?') === -1 ? '?' : '&') + hash;
|
|
1028
|
-
}
|
|
1029
971
|
|
|
1030
|
-
|
|
1031
|
-
* Encode each path component using `encodeURIComponent` as files can contain characters
|
|
1032
|
-
* which needs special encoding in URLs like `+ `.
|
|
1033
|
-
*
|
|
1034
|
-
* Valid filesystem characters which need to be encoded for urls:
|
|
1035
|
-
*
|
|
1036
|
-
* # pound, % percent, & ampersand, { left curly bracket, } right curly bracket,
|
|
1037
|
-
* \ back slash, < left angle bracket, > right angle bracket, * asterisk, ? question mark,
|
|
1038
|
-
* blank spaces, $ dollar sign, ! exclamation point, ' single quotes, " double quotes,
|
|
1039
|
-
* : colon, @ at sign, + plus sign, ` backtick, | pipe, = equal sign
|
|
1040
|
-
*
|
|
1041
|
-
* However the query string must not be encoded:
|
|
1042
|
-
*
|
|
1043
|
-
* fo:demonstration-path/very fancy+name.js?path=/home?value=abc&value=def#zzz
|
|
1044
|
-
* ^ ^ ^ ^ ^ ^ ^ ^^ ^ ^ ^ ^ ^
|
|
1045
|
-
* | | | | | | | || | | | | |
|
|
1046
|
-
* encoded | | encoded | | || | | | | |
|
|
1047
|
-
* ignored ignored ignored ignored ignored
|
|
1048
|
-
*
|
|
1049
|
-
* @param {string} filePath
|
|
1050
|
-
*/
|
|
1051
|
-
function urlencodePath (filePath) {
|
|
1052
|
-
// People use the filepath in quite unexpected ways.
|
|
1053
|
-
// Try to extract the first querystring of the url:
|
|
1054
|
-
//
|
|
1055
|
-
// some+path/demo.html?value=abc?def
|
|
1056
|
-
//
|
|
1057
|
-
const queryStringStart = filePath.indexOf('?');
|
|
1058
|
-
const urlPath = queryStringStart === -1 ? filePath : filePath.substr(0, queryStringStart);
|
|
1059
|
-
const queryString = filePath.substr(urlPath.length);
|
|
1060
|
-
// Encode all parts except '/' which are not part of the querystring:
|
|
1061
|
-
const encodedUrlPath = urlPath.split('/').map(encodeURIComponent).join('/');
|
|
1062
|
-
return encodedUrlPath + queryString;
|
|
1063
|
-
}
|
|
972
|
+
const contentHash = hash.digest(compilation.outputOptions.hashDigest).slice(0, compilation.outputOptions.hashDigestLength);
|
|
1064
973
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
if (template === 'auto') {
|
|
1074
|
-
template = path.resolve(context, 'src/index.ejs');
|
|
1075
|
-
if (!fs.existsSync(template)) {
|
|
1076
|
-
template = path.join(__dirname, 'default_index.ejs');
|
|
974
|
+
return compilation.getPathWithInfo(
|
|
975
|
+
filename,
|
|
976
|
+
{
|
|
977
|
+
contentHash,
|
|
978
|
+
chunk: {
|
|
979
|
+
hash: contentHash,
|
|
980
|
+
contentHash
|
|
981
|
+
}
|
|
1077
982
|
}
|
|
1078
|
-
|
|
1079
|
-
// If the template doesn't use a loader use the lodash template loader
|
|
1080
|
-
if (template.indexOf('!') === -1) {
|
|
1081
|
-
template = require.resolve('./lib/loader.js') + '!' + path.resolve(context, template);
|
|
1082
|
-
}
|
|
1083
|
-
// Resolve template path
|
|
1084
|
-
return template.replace(
|
|
1085
|
-
/([!])([^/\\][^!?]+|[^/\\!?])($|\?[^!?\n]+$)/,
|
|
1086
|
-
(match, prefix, filepath, postfix) => prefix + path.resolve(filepath) + postfix);
|
|
983
|
+
);
|
|
1087
984
|
}
|
|
1088
985
|
|
|
1089
986
|
/**
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1092
|
-
* As this is a breaking change to html-webpack-plugin 3.x
|
|
1093
|
-
* provide an extended error message to explain how to get back
|
|
1094
|
-
* to the old behaviour
|
|
987
|
+
* Function to generate HTML file.
|
|
1095
988
|
*
|
|
1096
|
-
* @
|
|
989
|
+
* @private
|
|
990
|
+
* @param {Compiler} compiler
|
|
991
|
+
* @param {Compilation} compilation
|
|
992
|
+
* @param {string} outputName
|
|
993
|
+
* @param {CachedChildCompilation} childCompilerPlugin
|
|
994
|
+
* @param {PreviousEmittedAssets} previousEmittedAssets
|
|
995
|
+
* @param {{ value: string | undefined }} assetJson
|
|
996
|
+
* @param {(err?: Error) => void} callback
|
|
1097
997
|
*/
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
998
|
+
generateHTML (
|
|
999
|
+
compiler,
|
|
1000
|
+
compilation,
|
|
1001
|
+
outputName,
|
|
1002
|
+
childCompilerPlugin,
|
|
1003
|
+
previousEmittedAssets,
|
|
1004
|
+
assetJson,
|
|
1005
|
+
callback
|
|
1006
|
+
) {
|
|
1007
|
+
// Get all entry point names for this html file
|
|
1008
|
+
const entryNames = Array.from(compilation.entrypoints.keys());
|
|
1009
|
+
const filteredEntryNames = this.filterEntryChunks(entryNames, this.options.chunks, this.options.excludeChunks);
|
|
1010
|
+
const sortedEntryNames = this.sortEntryChunks(filteredEntryNames, this.options.chunksSortMode, compilation);
|
|
1011
|
+
const templateResult = this.options.templateContent
|
|
1012
|
+
? { mainCompilationHash: compilation.hash }
|
|
1013
|
+
: childCompilerPlugin.getCompilationEntryResult(this.options.template);
|
|
1014
|
+
|
|
1015
|
+
if ('error' in templateResult) {
|
|
1016
|
+
compilation.errors.push(prettyError(templateResult.error, compiler.context).toString());
|
|
1101
1017
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1018
|
+
|
|
1019
|
+
// If the child compilation was not executed during a previous main compile run
|
|
1020
|
+
// it is a cached result
|
|
1021
|
+
const isCompilationCached = templateResult.mainCompilationHash !== compilation.hash;
|
|
1022
|
+
/** Generated file paths from the entry point names */
|
|
1023
|
+
const assetsInformationByGroups = this.getAssetsInformationByGroups(compilation, outputName, sortedEntryNames);
|
|
1024
|
+
// If the template and the assets did not change we don't have to emit the html
|
|
1025
|
+
const newAssetJson = JSON.stringify(this.getAssetFiles(assetsInformationByGroups));
|
|
1026
|
+
|
|
1027
|
+
if (isCompilationCached && this.options.cache && assetJson.value === newAssetJson) {
|
|
1028
|
+
previousEmittedAssets.forEach(({ name, source, info }) => {
|
|
1029
|
+
compilation.emitAsset(name, source, info);
|
|
1030
|
+
});
|
|
1031
|
+
return callback();
|
|
1032
|
+
} else {
|
|
1033
|
+
previousEmittedAssets.length = 0;
|
|
1034
|
+
assetJson.value = newAssetJson;
|
|
1118
1035
|
}
|
|
1119
|
-
}
|
|
1120
1036
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1037
|
+
// The html-webpack plugin uses a object representation for the html-tags which will be injected
|
|
1038
|
+
// to allow altering them more easily
|
|
1039
|
+
// Just before they are converted a third-party-plugin author might change the order and content
|
|
1040
|
+
const assetsPromise = this.generateFavicon(compiler, this.options.favicon, compilation, assetsInformationByGroups.publicPath, previousEmittedAssets)
|
|
1041
|
+
.then((faviconPath) => {
|
|
1042
|
+
assetsInformationByGroups.favicon = faviconPath;
|
|
1043
|
+
return getHtmlWebpackPluginHooks(compilation).beforeAssetTagGeneration.promise({
|
|
1044
|
+
assets: assetsInformationByGroups,
|
|
1045
|
+
outputName,
|
|
1046
|
+
plugin: this
|
|
1047
|
+
});
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
// Turn the js and css paths into grouped HtmlTagObjects
|
|
1051
|
+
const assetTagGroupsPromise = assetsPromise
|
|
1052
|
+
// And allow third-party-plugin authors to reorder and change the assetTags before they are grouped
|
|
1053
|
+
.then(({ assets }) => getHtmlWebpackPluginHooks(compilation).alterAssetTags.promise({
|
|
1054
|
+
assetTags: {
|
|
1055
|
+
scripts: this.generatedScriptTags(assets.js),
|
|
1056
|
+
styles: this.generateStyleTags(assets.css),
|
|
1057
|
+
meta: [
|
|
1058
|
+
...(this.options.base !== false ? this.generateBaseTag(this.options.base) : []),
|
|
1059
|
+
...this.generatedMetaTags(this.options.meta),
|
|
1060
|
+
...(assets.favicon ? this.generateFaviconTag(assets.favicon) : [])
|
|
1061
|
+
]
|
|
1062
|
+
},
|
|
1063
|
+
outputName,
|
|
1064
|
+
publicPath: assetsInformationByGroups.publicPath,
|
|
1065
|
+
plugin: this
|
|
1066
|
+
}))
|
|
1067
|
+
.then(({ assetTags }) => {
|
|
1068
|
+
// Inject scripts to body unless it set explicitly to head
|
|
1069
|
+
const scriptTarget = this.options.inject === 'head' ||
|
|
1070
|
+
(this.options.inject !== 'body' && this.options.scriptLoading !== 'blocking') ? 'head' : 'body';
|
|
1071
|
+
// Group assets to `head` and `body` tag arrays
|
|
1072
|
+
const assetGroups = this.groupAssetsByElements(assetTags, scriptTarget);
|
|
1073
|
+
// Allow third-party-plugin authors to reorder and change the assetTags once they are grouped
|
|
1074
|
+
return getHtmlWebpackPluginHooks(compilation).alterAssetTagGroups.promise({
|
|
1075
|
+
headTags: assetGroups.headTags,
|
|
1076
|
+
bodyTags: assetGroups.bodyTags,
|
|
1077
|
+
outputName,
|
|
1078
|
+
publicPath: assetsInformationByGroups.publicPath,
|
|
1079
|
+
plugin: this
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
// Turn the compiled template into a nodejs function or into a nodejs string
|
|
1084
|
+
const templateEvaluationPromise = Promise.resolve()
|
|
1085
|
+
.then(() => {
|
|
1086
|
+
if ('error' in templateResult) {
|
|
1087
|
+
return this.options.showErrors ? prettyError(templateResult.error, compiler.context).toHtml() : 'ERROR';
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Allow to use a custom function / string instead
|
|
1091
|
+
if (this.options.templateContent !== false) {
|
|
1092
|
+
return this.options.templateContent;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Once everything is compiled evaluate the html factory and replace it with its content
|
|
1096
|
+
if ('compiledEntry' in templateResult) {
|
|
1097
|
+
const compiledEntry = templateResult.compiledEntry;
|
|
1098
|
+
const assets = compiledEntry.assets;
|
|
1099
|
+
|
|
1100
|
+
// Store assets from child compiler to reemit them later
|
|
1101
|
+
for (const name in assets) {
|
|
1102
|
+
previousEmittedAssets.push({ name, source: assets[name].source, info: assets[name].info });
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return this.evaluateCompilationResult(compiledEntry.content, assetsInformationByGroups.publicPath, this.options.template);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return Promise.reject(new Error('Child compilation contained no compiledEntry'));
|
|
1109
|
+
});
|
|
1110
|
+
const templateExectutionPromise = Promise.all([assetsPromise, assetTagGroupsPromise, templateEvaluationPromise])
|
|
1111
|
+
// Execute the template
|
|
1112
|
+
.then(([assetsHookResult, assetTags, compilationResult]) => typeof compilationResult !== 'function'
|
|
1113
|
+
? compilationResult
|
|
1114
|
+
: this.executeTemplate(compilationResult, assetsHookResult.assets, { headTags: assetTags.headTags, bodyTags: assetTags.bodyTags }, compilation));
|
|
1115
|
+
|
|
1116
|
+
const injectedHtmlPromise = Promise.all([assetTagGroupsPromise, templateExectutionPromise])
|
|
1117
|
+
// Allow plugins to change the html before assets are injected
|
|
1118
|
+
.then(([assetTags, html]) => {
|
|
1119
|
+
const pluginArgs = { html, headTags: assetTags.headTags, bodyTags: assetTags.bodyTags, plugin: this, outputName };
|
|
1120
|
+
return getHtmlWebpackPluginHooks(compilation).afterTemplateExecution.promise(pluginArgs);
|
|
1121
|
+
})
|
|
1122
|
+
.then(({ html, headTags, bodyTags }) => {
|
|
1123
|
+
return this.postProcessHtml(compiler, html, assetsInformationByGroups, { headTags, bodyTags });
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
const emitHtmlPromise = injectedHtmlPromise
|
|
1127
|
+
// Allow plugins to change the html after assets are injected
|
|
1128
|
+
.then((html) => {
|
|
1129
|
+
const pluginArgs = { html, plugin: this, outputName };
|
|
1130
|
+
return getHtmlWebpackPluginHooks(compilation).beforeEmit.promise(pluginArgs)
|
|
1131
|
+
.then(result => result.html);
|
|
1132
|
+
})
|
|
1133
|
+
.catch(err => {
|
|
1134
|
+
// In case anything went wrong the promise is resolved
|
|
1135
|
+
// with the error message and an error is logged
|
|
1136
|
+
compilation.errors.push(prettyError(err, compiler.context).toString());
|
|
1137
|
+
return this.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
|
|
1138
|
+
})
|
|
1139
|
+
.then(html => {
|
|
1140
|
+
const filename = outputName.replace(/\[templatehash([^\]]*)\]/g, require('util').deprecate(
|
|
1141
|
+
(match, options) => `[contenthash${options}]`,
|
|
1142
|
+
'[templatehash] is now [contenthash]')
|
|
1143
|
+
);
|
|
1144
|
+
const replacedFilename = this.replacePlaceholdersInFilename(compiler, filename, html, compilation);
|
|
1145
|
+
const source = new compiler.webpack.sources.RawSource(html, false);
|
|
1146
|
+
|
|
1147
|
+
// Add the evaluated html code to the webpack assets
|
|
1148
|
+
compilation.emitAsset(replacedFilename.path, source, replacedFilename.info);
|
|
1149
|
+
previousEmittedAssets.push({ name: replacedFilename.path, source });
|
|
1150
|
+
|
|
1151
|
+
return replacedFilename.path;
|
|
1152
|
+
})
|
|
1153
|
+
.then((finalOutputName) => getHtmlWebpackPluginHooks(compilation).afterEmit.promise({
|
|
1154
|
+
outputName: finalOutputName,
|
|
1155
|
+
plugin: this
|
|
1156
|
+
}).catch(err => {
|
|
1157
|
+
/** @type {Logger} */
|
|
1158
|
+
(this.logger).error(err);
|
|
1159
|
+
return null;
|
|
1160
|
+
}).then(() => null));
|
|
1161
|
+
|
|
1162
|
+
// Once all files are added to the webpack compilation
|
|
1163
|
+
// let the webpack compiler continue
|
|
1164
|
+
emitHtmlPromise.then(() => {
|
|
1165
|
+
callback();
|
|
1166
|
+
});
|
|
1129
1167
|
}
|
|
1130
1168
|
}
|
|
1131
1169
|
|
|
@@ -1134,14 +1172,8 @@ function hookIntoCompiler (compiler, options, plugin) {
|
|
|
1134
1172
|
* Generate the template parameters
|
|
1135
1173
|
*
|
|
1136
1174
|
* Generate the template parameters for the template function
|
|
1137
|
-
* @param {
|
|
1138
|
-
* @param {
|
|
1139
|
-
publicPath: string,
|
|
1140
|
-
js: Array<string>,
|
|
1141
|
-
css: Array<string>,
|
|
1142
|
-
manifest?: string,
|
|
1143
|
-
favicon?: string
|
|
1144
|
-
}} assets
|
|
1175
|
+
* @param {Compilation} compilation
|
|
1176
|
+
* @param {AssetsInformationByGroups} assets
|
|
1145
1177
|
* @param {{
|
|
1146
1178
|
headTags: HtmlTagObject[],
|
|
1147
1179
|
bodyTags: HtmlTagObject[]
|