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