html-webpack-plugin 5.5.3 → 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 +2 -1
- package/index.js +821 -791
- 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 +1 -1
- package/typings.d.ts +2 -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,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') {
|
|
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
|
-
|
|
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,201 @@ 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';
|
|
793
797
|
}
|
|
794
|
-
|
|
798
|
+
|
|
799
|
+
attributes.src = src;
|
|
800
|
+
|
|
801
|
+
return {
|
|
802
|
+
tagName: 'script',
|
|
803
|
+
voidTag: false,
|
|
804
|
+
meta: { plugin: 'html-webpack-plugin' },
|
|
805
|
+
attributes
|
|
806
|
+
};
|
|
807
|
+
});
|
|
795
808
|
}
|
|
796
809
|
|
|
797
810
|
/**
|
|
798
811
|
* Generate all style tags for the given file paths
|
|
812
|
+
*
|
|
813
|
+
* @private
|
|
799
814
|
* @param {Array<string>} cssAssets
|
|
800
815
|
* @returns {Array<HtmlTagObject>}
|
|
801
816
|
*/
|
|
802
|
-
|
|
817
|
+
generateStyleTags (cssAssets) {
|
|
803
818
|
return cssAssets.map(styleAsset => ({
|
|
804
819
|
tagName: 'link',
|
|
805
820
|
voidTag: true,
|
|
@@ -813,41 +828,34 @@ function hookIntoCompiler (compiler, options, plugin) {
|
|
|
813
828
|
|
|
814
829
|
/**
|
|
815
830
|
* 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
|
-
}
|
|
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
|
+
}];
|
|
835
845
|
}
|
|
836
846
|
|
|
837
847
|
/**
|
|
838
848
|
* Generate all meta tags for the given meta configuration
|
|
839
|
-
*
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
* @returns {Array<HtmlTagObject>}
|
|
846
|
-
*/
|
|
847
|
-
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) {
|
|
848
855
|
if (metaOptions === false) {
|
|
849
856
|
return [];
|
|
850
857
|
}
|
|
858
|
+
|
|
851
859
|
// Make tags self-closing in case of xhtml
|
|
852
860
|
// Turn { "viewport" : "width=500, initial-scale=1" } into
|
|
853
861
|
// [{ name:"viewport" content:"width=500, initial-scale=1" }]
|
|
@@ -860,8 +868,9 @@ function hookIntoCompiler (compiler, options, plugin) {
|
|
|
860
868
|
} : metaTagContent;
|
|
861
869
|
})
|
|
862
870
|
.filter((attribute) => attribute !== false);
|
|
863
|
-
|
|
864
|
-
|
|
871
|
+
|
|
872
|
+
// Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
|
|
873
|
+
// the html-webpack-plugin tag structure
|
|
865
874
|
return metaTagAttributeObjects.map((metaTagAttributes) => {
|
|
866
875
|
if (metaTagAttributes === false) {
|
|
867
876
|
throw new Error('Invalid meta tag');
|
|
@@ -877,39 +886,38 @@ function hookIntoCompiler (compiler, options, plugin) {
|
|
|
877
886
|
|
|
878
887
|
/**
|
|
879
888
|
* Generate a favicon tag for the given file path
|
|
880
|
-
*
|
|
889
|
+
*
|
|
890
|
+
* @private
|
|
891
|
+
* @param {string} favicon
|
|
881
892
|
* @returns {Array<HtmlTagObject>}
|
|
882
893
|
*/
|
|
883
|
-
|
|
884
|
-
if (!faviconPath) {
|
|
885
|
-
return [];
|
|
886
|
-
}
|
|
894
|
+
generateFaviconTag (favicon) {
|
|
887
895
|
return [{
|
|
888
896
|
tagName: 'link',
|
|
889
897
|
voidTag: true,
|
|
890
898
|
meta: { plugin: 'html-webpack-plugin' },
|
|
891
899
|
attributes: {
|
|
892
900
|
rel: 'icon',
|
|
893
|
-
href:
|
|
901
|
+
href: favicon
|
|
894
902
|
}
|
|
895
903
|
}];
|
|
896
904
|
}
|
|
897
905
|
|
|
898
906
|
/**
|
|
899
|
-
* Group assets to head and
|
|
907
|
+
* Group assets to head and body tags
|
|
900
908
|
*
|
|
901
909
|
* @param {{
|
|
902
910
|
scripts: Array<HtmlTagObject>;
|
|
903
911
|
styles: Array<HtmlTagObject>;
|
|
904
912
|
meta: Array<HtmlTagObject>;
|
|
905
913
|
}} assetTags
|
|
906
|
-
|
|
907
|
-
|
|
914
|
+
* @param {"body" | "head"} scriptTarget
|
|
915
|
+
* @returns {{
|
|
908
916
|
headTags: Array<HtmlTagObject>;
|
|
909
917
|
bodyTags: Array<HtmlTagObject>;
|
|
910
918
|
}}
|
|
911
|
-
|
|
912
|
-
|
|
919
|
+
*/
|
|
920
|
+
groupAssetsByElements (assetTags, scriptTarget) {
|
|
913
921
|
/** @type {{ headTags: Array<HtmlTagObject>; bodyTags: Array<HtmlTagObject>; }} */
|
|
914
922
|
const result = {
|
|
915
923
|
headTags: [
|
|
@@ -918,214 +926,242 @@ function hookIntoCompiler (compiler, options, plugin) {
|
|
|
918
926
|
],
|
|
919
927
|
bodyTags: []
|
|
920
928
|
};
|
|
929
|
+
|
|
921
930
|
// Add script tags to head or body depending on
|
|
922
931
|
// the htmlPluginOptions
|
|
923
932
|
if (scriptTarget === 'body') {
|
|
924
933
|
result.bodyTags.push(...assetTags.scripts);
|
|
925
934
|
} else {
|
|
926
935
|
// 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;
|
|
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
|
+
|
|
929
939
|
result.headTags.splice(insertPosition, 0, ...assetTags.scripts);
|
|
930
940
|
}
|
|
931
|
-
return result;
|
|
932
|
-
}
|
|
933
941
|
|
|
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
|
-
}));
|
|
942
|
+
return result;
|
|
950
943
|
}
|
|
951
944
|
|
|
952
945
|
/**
|
|
953
|
-
*
|
|
946
|
+
* Replace [contenthash] in filename
|
|
954
947
|
*
|
|
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
|
|
948
|
+
* @see https://survivejs.com/webpack/optimizing/adding-hashes-to-filenames/
|
|
963
949
|
*
|
|
964
|
-
* @
|
|
950
|
+
* @private
|
|
951
|
+
* @param {Compiler} compiler
|
|
952
|
+
* @param {string} filename
|
|
953
|
+
* @param {string|Buffer} fileContent
|
|
954
|
+
* @param {Compilation} compilation
|
|
955
|
+
* @returns {{ path: string, info: {} }}
|
|
965
956
|
*/
|
|
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
|
-
}
|
|
957
|
+
replacePlaceholdersInFilename (compiler, filename, fileContent, compilation) {
|
|
958
|
+
if (/\[\\*([\w:]+)\\*\]/i.test(filename) === false) {
|
|
959
|
+
return { path: filename, info: {} };
|
|
988
960
|
}
|
|
989
961
|
|
|
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
|
-
}
|
|
962
|
+
const hash = compiler.webpack.util.createHash(compilation.outputOptions.hashFunction);
|
|
1003
963
|
|
|
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
|
-
}
|
|
964
|
+
hash.update(fileContent);
|
|
1016
965
|
|
|
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;
|
|
966
|
+
if (compilation.outputOptions.hashSalt) {
|
|
967
|
+
hash.update(compilation.outputOptions.hashSalt);
|
|
1026
968
|
}
|
|
1027
|
-
return url + (url.indexOf('?') === -1 ? '?' : '&') + hash;
|
|
1028
|
-
}
|
|
1029
969
|
|
|
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
|
-
}
|
|
970
|
+
const contentHash = hash.digest(compilation.outputOptions.hashDigest).slice(0, compilation.outputOptions.hashDigestLength);
|
|
1064
971
|
|
|
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');
|
|
972
|
+
return compilation.getPathWithInfo(
|
|
973
|
+
filename,
|
|
974
|
+
{
|
|
975
|
+
contentHash,
|
|
976
|
+
chunk: {
|
|
977
|
+
hash: contentHash,
|
|
978
|
+
contentHash
|
|
979
|
+
}
|
|
1077
980
|
}
|
|
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);
|
|
981
|
+
);
|
|
1087
982
|
}
|
|
1088
983
|
|
|
1089
984
|
/**
|
|
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
|
|
985
|
+
* Function to generate HTML file.
|
|
1095
986
|
*
|
|
1096
|
-
* @
|
|
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
|
|
1097
995
|
*/
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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());
|
|
1101
1015
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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;
|
|
1118
1033
|
}
|
|
1119
|
-
}
|
|
1120
1034
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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
|
+
});
|
|
1129
1165
|
}
|
|
1130
1166
|
}
|
|
1131
1167
|
|
|
@@ -1134,14 +1170,8 @@ function hookIntoCompiler (compiler, options, plugin) {
|
|
|
1134
1170
|
* Generate the template parameters
|
|
1135
1171
|
*
|
|
1136
1172
|
* 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
|
|
1173
|
+
* @param {Compilation} compilation
|
|
1174
|
+
* @param {AssetsInformationByGroups} assets
|
|
1145
1175
|
* @param {{
|
|
1146
1176
|
headTags: HtmlTagObject[],
|
|
1147
1177
|
bodyTags: HtmlTagObject[]
|