html-webpack-plugin 3.1.0 → 4.0.0-alpha.2

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/index.js CHANGED
@@ -1,3 +1,10 @@
1
+ // @ts-check
2
+ // Import types
3
+ /* eslint-disable */
4
+ /// <reference path="./typings.d.ts" />
5
+ /* eslint-enable */
6
+ /** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */
7
+ /** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
1
8
  'use strict';
2
9
 
3
10
  // use Polyfill for util.promisify in node versions < v8
@@ -7,18 +14,31 @@ const vm = require('vm');
7
14
  const fs = require('fs');
8
15
  const _ = require('lodash');
9
16
  const path = require('path');
17
+ const loaderUtils = require('loader-utils');
18
+
19
+ const { createHtmlTagObject, htmlTagObjectToString } = require('./lib/html-tags');
20
+
10
21
  const childCompiler = require('./lib/compiler.js');
11
22
  const prettyError = require('./lib/errors.js');
12
23
  const chunkSorter = require('./lib/chunksorter.js');
24
+ const getHtmlWebpackPluginHooks = require('./lib/hooks.js').getHtmlWebpackPluginHooks;
13
25
 
14
26
  const fsStatAsync = promisify(fs.stat);
15
27
  const fsReadFileAsync = promisify(fs.readFile);
16
28
 
17
29
  class HtmlWebpackPlugin {
30
+ /**
31
+ * @param {Partial<HtmlWebpackPluginOptions>} [options]
32
+ */
18
33
  constructor (options) {
34
+ /** @type {Partial<HtmlWebpackPluginOptions>} */
35
+ const userOptions = options || {};
36
+
19
37
  // Default options
20
- this.options = _.extend({
38
+ /** @type {HtmlWebpackPluginOptions} */
39
+ const defaultOptions = {
21
40
  template: path.join(__dirname, 'default_index.ejs'),
41
+ templateContent: false,
22
42
  templateParameters: templateParametersGenerator,
23
43
  filename: 'index.html',
24
44
  hash: false,
@@ -30,14 +50,44 @@ class HtmlWebpackPlugin {
30
50
  showErrors: true,
31
51
  chunks: 'all',
32
52
  excludeChunks: [],
53
+ chunksSortMode: 'auto',
54
+ meta: {},
33
55
  title: 'Webpack App',
34
56
  xhtml: false
35
- }, options);
57
+ };
58
+
59
+ /** @type {HtmlWebpackPluginOptions} */
60
+ this.options = Object.assign(defaultOptions, userOptions);
61
+
62
+ // Default metaOptions if no template is provided
63
+ if (!userOptions.template && this.options.templateContent === false && this.options.meta) {
64
+ const defaultMeta = {
65
+ // From https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag
66
+ viewport: 'width=device-width, initial-scale=1'
67
+ };
68
+ this.options.meta = Object.assign({}, this.options.meta, defaultMeta, userOptions.meta);
69
+ }
70
+
71
+ // Instance variables to keep caching information
72
+ // for multiple builds
73
+ this.childCompilerHash = undefined;
74
+ /**
75
+ * @type {string | undefined}
76
+ */
77
+ this.childCompilationOutputName = undefined;
78
+ this.assetJson = undefined;
79
+ this.hash = undefined;
80
+ this.version = HtmlWebpackPlugin.version;
36
81
  }
37
82
 
83
+ /**
84
+ * apply is called by the webpack main compiler during the start phase
85
+ * @param {WebpackCompiler} compiler
86
+ */
38
87
  apply (compiler) {
39
88
  const self = this;
40
89
  let isCompilationCached = false;
90
+ /** @type Promise<string> */
41
91
  let compilationPromise;
42
92
 
43
93
  this.options.template = this.getFullTemplatePath(this.options.template, compiler.context);
@@ -49,34 +99,41 @@ class HtmlWebpackPlugin {
49
99
  this.options.filename = path.relative(compiler.options.output.path, filename);
50
100
  }
51
101
 
52
- // setup hooks for webpack 4
53
- if (compiler.hooks) {
54
- compiler.hooks.compilation.tap('HtmlWebpackPluginHooks', compilation => {
55
- const SyncWaterfallHook = require('tapable').SyncWaterfallHook;
56
- const AsyncSeriesWaterfallHook = require('tapable').AsyncSeriesWaterfallHook;
57
- compilation.hooks.htmlWebpackPluginAlterChunks = new SyncWaterfallHook(['chunks', 'objectWithPluginRef']);
58
- compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration = new AsyncSeriesWaterfallHook(['pluginArgs']);
59
- compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing = new AsyncSeriesWaterfallHook(['pluginArgs']);
60
- compilation.hooks.htmlWebpackPluginAlterAssetTags = new AsyncSeriesWaterfallHook(['pluginArgs']);
61
- compilation.hooks.htmlWebpackPluginAfterHtmlProcessing = new AsyncSeriesWaterfallHook(['pluginArgs']);
62
- compilation.hooks.htmlWebpackPluginAfterEmit = new AsyncSeriesWaterfallHook(['pluginArgs']);
102
+ // Clear the cache once a new HtmlWebpackPlugin is added
103
+ childCompiler.clearCache(compiler);
104
+
105
+ // Register all HtmlWebpackPlugins instances at the child compiler
106
+ compiler.hooks.thisCompilation.tap('HtmlWebpackPlugin', (compilation) => {
107
+ // Clear the cache if the child compiler is outdated
108
+ if (childCompiler.hasOutDatedTemplateCache(compilation)) {
109
+ childCompiler.clearCache(compiler);
110
+ }
111
+ // Add this instances template to the child compiler
112
+ childCompiler.addTemplateToCompiler(compiler, this.options.template);
113
+ // Add file dependencies of child compiler to parent compiler
114
+ // to keep them watched even if we get the result from the cache
115
+ compilation.hooks.additionalChunkAssets.tap('HtmlWebpackPlugin', () => {
116
+ const childCompilerDependencies = childCompiler.getFileDependencies(compiler);
117
+ childCompilerDependencies.forEach(fileDependency => {
118
+ compilation.compilationDependencies.add(fileDependency);
119
+ });
63
120
  });
64
- }
121
+ });
65
122
 
66
- // Backwards compatible version of: compiler.hooks.make.tapAsync()
67
- (compiler.hooks ? compiler.hooks.make.tapAsync.bind(compiler.hooks.make, 'HtmlWebpackPlugin') : compiler.plugin.bind(compiler, 'make'))((compilation, callback) => {
123
+ compiler.hooks.make.tapAsync('HtmlWebpackPlugin', (compilation, callback) => {
68
124
  // Compile the template (queued)
69
- compilationPromise = childCompiler.compileTemplate(self.options.template, compiler.context, self.options.filename, compilation)
125
+ compilationPromise = childCompiler.compileTemplate(self.options.template, self.options.filename, compilation)
70
126
  .catch(err => {
71
127
  compilation.errors.push(prettyError(err, compiler.context).toString());
72
128
  return {
73
129
  content: self.options.showErrors ? prettyError(err, compiler.context).toJsonHtml() : 'ERROR',
74
- outputName: self.options.filename
130
+ outputName: self.options.filename,
131
+ hash: ''
75
132
  };
76
133
  })
77
134
  .then(compilationResult => {
78
135
  // If the compilation change didnt change the cache is valid
79
- isCompilationCached = compilationResult.hash && self.childCompilerHash === compilationResult.hash;
136
+ isCompilationCached = Boolean(compilationResult.hash) && self.childCompilerHash === compilationResult.hash;
80
137
  self.childCompilerHash = compilationResult.hash;
81
138
  self.childCompilationOutputName = compilationResult.outputName;
82
139
  callback();
@@ -84,155 +141,166 @@ class HtmlWebpackPlugin {
84
141
  });
85
142
  });
86
143
 
87
- // Backwards compatible version of: compiler.plugin.emit.tapAsync()
88
- (compiler.hooks ? compiler.hooks.emit.tapAsync.bind(compiler.hooks.emit, 'HtmlWebpackPlugin') : compiler.plugin.bind(compiler, 'emit'))((compilation, callback) => {
89
- const applyPluginsAsyncWaterfall = self.applyPluginsAsyncWaterfall(compilation);
90
- // Get chunks info as json
91
- // Note: we're excluding stuff that we don't need to improve toJson serialization speed.
92
- const chunkOnlyConfig = {
93
- assets: false,
94
- cached: false,
95
- children: false,
96
- chunks: true,
97
- chunkModules: false,
98
- chunkOrigins: false,
99
- errorDetails: false,
100
- hash: false,
101
- modules: false,
102
- reasons: false,
103
- source: false,
104
- timings: false,
105
- version: false
106
- };
107
- const allChunks = compilation.getStats().toJson(chunkOnlyConfig).chunks;
108
- // Filter chunks (options.chunks and options.excludeCHunks)
109
- let chunks = self.filterChunks(allChunks, self.options.chunks, self.options.excludeChunks);
110
- // Sort chunks
111
- chunks = self.sortChunks(chunks, self.options.chunksSortMode, compilation.chunkGroups);
112
- // Let plugins alter the chunks and the chunk sorting
113
- if (compilation.hooks) {
114
- chunks = compilation.hooks.htmlWebpackPluginAlterChunks.call(chunks, { plugin: self });
115
- } else {
116
- // Before Webpack 4
117
- chunks = compilation.applyPluginsWaterfall('html-webpack-plugin-alter-chunks', chunks, { plugin: self });
118
- }
119
- // Get assets
120
- const assets = self.htmlWebpackPluginAssets(compilation, chunks);
121
- // If this is a hot update compilation, move on!
122
- // This solves a problem where an `index.html` file is generated for hot-update js files
123
- // It only happens in Webpack 2, where hot updates are emitted separately before the full bundle
124
- if (self.isHotUpdateCompilation(assets)) {
125
- return callback();
126
- }
144
+ compiler.hooks.emit.tapAsync('HtmlWebpackPlugin',
145
+ /**
146
+ * Hook into the webpack emit phase
147
+ * @param {WebpackCompilation} compilation
148
+ * @param {() => void} callback
149
+ */
150
+ (compilation, callback) => {
151
+ // Get all entry point names for this html file
152
+ const entryNames = Array.from(compilation.entrypoints.keys());
153
+ const filteredEntryNames = self.filterChunks(entryNames, self.options.chunks, self.options.excludeChunks);
154
+ const sortedEntryNames = self.sortEntryChunks(filteredEntryNames, this.options.chunksSortMode, compilation);
155
+ const childCompilationOutputName = self.childCompilationOutputName;
156
+
157
+ if (childCompilationOutputName === undefined) {
158
+ throw new Error('Did not receive child compilation result');
159
+ }
127
160
 
128
- // If the template and the assets did not change we don't have to emit the html
129
- const assetJson = JSON.stringify(self.getAssetFiles(assets));
130
- if (isCompilationCached && self.options.cache && assetJson === self.assetJson) {
131
- return callback();
132
- } else {
133
- self.assetJson = assetJson;
134
- }
161
+ // Turn the entry point names into file paths
162
+ const assets = self.htmlWebpackPluginAssets(compilation, childCompilationOutputName, sortedEntryNames);
163
+
164
+ // If this is a hot update compilation, move on!
165
+ // This solves a problem where an `index.html` file is generated for hot-update js files
166
+ // It only happens in Webpack 2, where hot updates are emitted separately before the full bundle
167
+ if (self.isHotUpdateCompilation(assets)) {
168
+ return callback();
169
+ }
170
+
171
+ // If the template and the assets did not change we don't have to emit the html
172
+ const assetJson = JSON.stringify(self.getAssetFiles(assets));
173
+ if (isCompilationCached && self.options.cache && assetJson === self.assetJson) {
174
+ return callback();
175
+ } else {
176
+ self.assetJson = assetJson;
177
+ }
135
178
 
136
- Promise.resolve()
137
- // Favicon
138
- .then(() => {
139
- if (self.options.favicon) {
140
- return self.addFileToAssets(self.options.favicon, compilation)
141
- .then(faviconBasename => {
142
- let publicPath = compilation.mainTemplate.getPublicPath({hash: compilation.hash}) || '';
143
- if (publicPath && publicPath.substr(-1) !== '/') {
144
- publicPath += '/';
145
- }
146
- assets.favicon = publicPath + faviconBasename;
179
+ // The html-webpack plugin uses a object representation for the html-tags which will be injected
180
+ // to allow altering them more easily
181
+ // Just before they are converted a third-party-plugin author might change the order and content
182
+ const assetsPromise = this.getFaviconPublicPath(this.options.favicon, compilation, assets.publicPath)
183
+ .then((faviconPath) => {
184
+ assets.favicon = faviconPath;
185
+ return getHtmlWebpackPluginHooks(compilation).beforeAssetTagGeneration.promise({
186
+ assets: assets,
187
+ outputName: childCompilationOutputName,
188
+ plugin: self
189
+ });
190
+ });
191
+
192
+ // Turn the js and css paths into grouped HtmlTagObjects
193
+ const assetTagGroupsPromise = assetsPromise
194
+ // And allow third-party-plugin authors to reorder and change the assetTags before they are grouped
195
+ .then(({assets}) => getHtmlWebpackPluginHooks(compilation).alterAssetTags.promise({
196
+ assetTags: {
197
+ scripts: self.generatedScriptTags(assets.js),
198
+ styles: self.generateStyleTags(assets.css),
199
+ meta: [
200
+ ...self.generatedMetaTags(self.options.meta),
201
+ ...self.generateFaviconTags(assets.favicon)
202
+ ]
203
+ },
204
+ outputName: childCompilationOutputName,
205
+ plugin: self
206
+ }))
207
+ .then(({assetTags}) => {
208
+ // Inject scripts to body unless it set explictly to head
209
+ const scriptTarget = self.options.inject === 'head' ? 'head' : 'body';
210
+ // Group assets to `head` and `body` tag arrays
211
+ const assetGroups = this.generateAssetGroups(assetTags, scriptTarget);
212
+ // Allow third-party-plugin authors to reorder and change the assetTags once they are grouped
213
+ return getHtmlWebpackPluginHooks(compilation).alterAssetTagGroups.promise({
214
+ headTags: assetGroups.headTags,
215
+ bodyTags: assetGroups.bodyTags,
216
+ outputName: childCompilationOutputName,
217
+ plugin: self
218
+ });
219
+ });
220
+
221
+ // Turn the compiled tempalte into a nodejs function or into a nodejs string
222
+ const templateEvaluationPromise = compilationPromise
223
+ .then(compiledTemplate => {
224
+ // Allow to use a custom function / string instead
225
+ if (self.options.templateContent !== false) {
226
+ return self.options.templateContent;
227
+ }
228
+ // Once everything is compiled evaluate the html factory
229
+ // and replace it with its content
230
+ return self.evaluateCompilationResult(compilation, compiledTemplate);
231
+ });
232
+
233
+ const templateExectutionPromise = Promise.all([assetsPromise, assetTagGroupsPromise, templateEvaluationPromise])
234
+ // Execute the template
235
+ .then(([assetsHookResult, assetTags, compilationResult]) => typeof compilationResult !== 'function'
236
+ ? compilationResult
237
+ : self.executeTemplate(compilationResult, assetsHookResult.assets, { headTags: assetTags.headTags, bodyTags: assetTags.bodyTags }, compilation));
238
+
239
+ const injectedHtmlPromise = Promise.all([assetTagGroupsPromise, templateExectutionPromise])
240
+ // Allow plugins to change the html before assets are injected
241
+ .then(([assetTags, html]) => {
242
+ const pluginArgs = {html, headTags: assetTags.headTags, bodyTags: assetTags.bodyTags, plugin: self, outputName: childCompilationOutputName};
243
+ return getHtmlWebpackPluginHooks(compilation).afterTemplateExecution.promise(pluginArgs);
244
+ })
245
+ .then(({html, headTags, bodyTags}) => {
246
+ return self.postProcessHtml(html, assets, {headTags, bodyTags});
247
+ });
248
+
249
+ const emitHtmlPromise = injectedHtmlPromise
250
+ // Allow plugins to change the html after assets are injected
251
+ .then((html) => {
252
+ const pluginArgs = {html, plugin: self, outputName: childCompilationOutputName};
253
+ return getHtmlWebpackPluginHooks(compilation).beforeEmit.promise(pluginArgs)
254
+ .then(result => result.html);
255
+ })
256
+ .catch(err => {
257
+ // In case anything went wrong the promise is resolved
258
+ // with the error message and an error is logged
259
+ compilation.errors.push(prettyError(err, compiler.context).toString());
260
+ // Prevent caching
261
+ self.hash = null;
262
+ return self.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
263
+ })
264
+ .then(html => {
265
+ // Allow to use [contenthash] as placeholder for the html-webpack-plugin name
266
+ // See also https://survivejs.com/webpack/optimizing/adding-hashes-to-filenames/
267
+ // From https://github.com/webpack-contrib/extract-text-webpack-plugin/blob/8de6558e33487e7606e7cd7cb2adc2cccafef272/src/index.js#L212-L214
268
+ const finalOutputName = childCompilationOutputName.replace(/\[(?:(\w+):)?contenthash(?::([a-z]+\d*))?(?::(\d+))?\]/ig, (_, hashType, digestType, maxLength) => {
269
+ return loaderUtils.getHashDigest(Buffer.from(html, 'utf8'), hashType, digestType, parseInt(maxLength, 10));
147
270
  });
148
- }
149
- })
150
- // Wait for the compilation to finish
151
- .then(() => compilationPromise)
152
- .then(compiledTemplate => {
153
- // Allow to use a custom function / string instead
154
- if (self.options.templateContent !== undefined) {
155
- return self.options.templateContent;
156
- }
157
- // Once everything is compiled evaluate the html factory
158
- // and replace it with its content
159
- return self.evaluateCompilationResult(compilation, compiledTemplate);
160
- })
161
- // Allow plugins to make changes to the assets before invoking the template
162
- // This only makes sense to use if `inject` is `false`
163
- .then(compilationResult => applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-generation', false, {
164
- assets: assets,
165
- outputName: self.childCompilationOutputName,
166
- plugin: self
167
- })
168
- .then(() => compilationResult))
169
- // Execute the template
170
- .then(compilationResult => typeof compilationResult !== 'function'
171
- ? compilationResult
172
- : self.executeTemplate(compilationResult, chunks, assets, compilation))
173
- // Allow plugins to change the html before assets are injected
174
- .then(html => {
175
- const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
176
- return applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-processing', true, pluginArgs);
177
- })
178
- .then(result => {
179
- const html = result.html;
180
- const assets = result.assets;
181
- // Prepare script and link tags
182
- const assetTags = self.generateAssetTags(assets);
183
- const pluginArgs = {head: assetTags.head, body: assetTags.body, plugin: self, chunks: chunks, outputName: self.childCompilationOutputName};
184
- // Allow plugins to change the assetTag definitions
185
- return applyPluginsAsyncWaterfall('html-webpack-plugin-alter-asset-tags', true, pluginArgs)
186
- .then(result => self.postProcessHtml(html, assets, { body: result.body, head: result.head })
187
- .then(html => _.extend(result, {html: html, assets: assets})));
188
- })
189
- // Allow plugins to change the html after assets are injected
190
- .then(result => {
191
- const html = result.html;
192
- const assets = result.assets;
193
- const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
194
- return applyPluginsAsyncWaterfall('html-webpack-plugin-after-html-processing', true, pluginArgs)
195
- .then(result => result.html);
196
- })
197
- .catch(err => {
198
- // In case anything went wrong the promise is resolved
199
- // with the error message and an error is logged
200
- compilation.errors.push(prettyError(err, compiler.context).toString());
201
- // Prevent caching
202
- self.hash = null;
203
- return self.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
204
- })
205
- .then(html => {
206
- // Replace the compilation result with the evaluated html code
207
- compilation.assets[self.childCompilationOutputName] = {
208
- source: () => html,
209
- size: () => html.length
210
- };
211
- })
212
- .then(() => applyPluginsAsyncWaterfall('html-webpack-plugin-after-emit', false, {
213
- html: compilation.assets[self.childCompilationOutputName],
214
- outputName: self.childCompilationOutputName,
215
- plugin: self
216
- }).catch(err => {
217
- console.error(err);
218
- return null;
219
- }).then(() => null))
220
- // Let webpack continue with it
221
- .then(() => {
271
+ // Add the evaluated html code to the webpack assets
272
+ compilation.assets[finalOutputName] = {
273
+ source: () => html,
274
+ size: () => html.length
275
+ };
276
+ return finalOutputName;
277
+ })
278
+ .then((finalOutputName) => getHtmlWebpackPluginHooks(compilation).afterEmit.promise({
279
+ outputName: finalOutputName,
280
+ plugin: self
281
+ }).catch(err => {
282
+ console.error(err);
283
+ return null;
284
+ }).then(() => null));
285
+
286
+ // Once all files are added to the webpack compilation
287
+ // let the webpack compiler continue
288
+ emitHtmlPromise.then(() => {
222
289
  callback();
223
290
  });
224
- });
291
+ });
225
292
  }
226
293
 
227
294
  /**
228
295
  * Evaluates the child compilation result
229
- * Returns a promise
296
+ * @param {WebpackCompilation} compilation
297
+ * @param {string} source
298
+ * @returns {Promise<string | (() => string | Promise<string>)>}
230
299
  */
231
300
  evaluateCompilationResult (compilation, source) {
232
301
  if (!source) {
233
302
  return Promise.reject('The child compilation didn\'t provide a result');
234
303
  }
235
-
236
304
  // The LibraryTemplatePlugin stores the template result in a local variable.
237
305
  // To extract the result during the evaluation this part has to be removed.
238
306
  source = source.replace('var HTML_WEBPACK_PLUGIN_RESULT =', '');
@@ -256,69 +324,101 @@ class HtmlWebpackPlugin {
256
324
 
257
325
  /**
258
326
  * Generate the template parameters for the template function
327
+ * @param {WebpackCompilation} compilation
328
+ * @param {{
329
+ publicPath: string,
330
+ js: Array<string>,
331
+ css: Array<string>,
332
+ manifest?: string,
333
+ favicon?: string
334
+ }} assets
335
+ * @param {{
336
+ headTags: HtmlTagObject[],
337
+ bodyTags: HtmlTagObject[]
338
+ }} assetTags
339
+ * @returns {{[key: any]: any}}
259
340
  */
260
- getTemplateParameters (compilation, assets) {
341
+ getTemplateParameters (compilation, assets, assetTags) {
342
+ if (this.options.templateParameters === false) {
343
+ return {};
344
+ }
261
345
  if (typeof this.options.templateParameters === 'function') {
262
- return this.options.templateParameters(compilation, assets, this.options);
346
+ return this.options.templateParameters(compilation, assets, assetTags, this.options);
263
347
  }
264
348
  if (typeof this.options.templateParameters === 'object') {
265
349
  return this.options.templateParameters;
266
350
  }
267
- return {};
351
+ throw new Error('templateParameters has to be either a function or an object');
268
352
  }
269
353
 
270
354
  /**
271
- * Html post processing
355
+ * This function renders the actual html by executing the template function
356
+ *
357
+ * @param {(templatePArameters) => string | Promise<string>} templateFunction
358
+ * @param {{
359
+ publicPath: string,
360
+ js: Array<string>,
361
+ css: Array<string>,
362
+ manifest?: string,
363
+ favicon?: string
364
+ }} assets
365
+ * @param {{
366
+ headTags: HtmlTagObject[],
367
+ bodyTags: HtmlTagObject[]
368
+ }} assetTags
369
+ * @param {WebpackCompilation} compilation
272
370
  *
273
- * Returns a promise
371
+ * @returns Promise<string>
274
372
  */
275
- executeTemplate (templateFunction, chunks, assets, compilation) {
276
- return Promise.resolve()
277
- // Template processing
278
- .then(() => {
279
- const templateParams = this.getTemplateParameters(compilation, assets);
280
- let html = '';
281
- try {
282
- html = templateFunction(templateParams);
283
- } catch (e) {
284
- compilation.errors.push(new Error('Template execution failed: ' + e));
285
- return Promise.reject(e);
286
- }
287
- return html;
288
- });
373
+ executeTemplate (templateFunction, assets, assetTags, compilation) {
374
+ // Template processing
375
+ const templateParams = this.getTemplateParameters(compilation, assets, assetTags);
376
+ /** @type {string|Promise<string>} */
377
+ let html = '';
378
+ try {
379
+ html = templateFunction(templateParams);
380
+ } catch (e) {
381
+ compilation.errors.push(new Error('Template execution failed: ' + e));
382
+ return Promise.reject(e);
383
+ }
384
+ // If html is a promise return the promise
385
+ // If html is a string turn it into a promise
386
+ return Promise.resolve().then(() => html);
289
387
  }
290
388
 
291
389
  /**
292
- * Html post processing
390
+ * Html Post processing
293
391
  *
294
- * Returns a promise
392
+ * @param {any} html
393
+ * The input html
394
+ * @param {any} assets
395
+ * @param {{
396
+ headTags: HtmlTagObject[],
397
+ bodyTags: HtmlTagObject[]
398
+ }} assetTags
399
+ * The asset tags to inject
400
+ *
401
+ * @returns {Promise<string>}
295
402
  */
296
403
  postProcessHtml (html, assets, assetTags) {
297
- const self = this;
298
404
  if (typeof html !== 'string') {
299
405
  return Promise.reject('Expected html to be a string but got ' + JSON.stringify(html));
300
406
  }
301
- return Promise.resolve()
302
- // Inject
303
- .then(() => {
304
- if (self.options.inject) {
305
- return self.injectAssetsIntoHtml(html, assets, assetTags);
306
- } else {
307
- return html;
308
- }
309
- })
310
- // Minify
311
- .then(html => {
312
- if (self.options.minify) {
313
- const minify = require('html-minifier').minify;
314
- return minify(html, self.options.minify);
315
- }
316
- return html;
317
- });
407
+ const htmlAfterInjection = this.options.inject
408
+ ? this.injectAssetsIntoHtml(html, assets, assetTags)
409
+ : html;
410
+ const htmlAfterMinification = this.options.minify
411
+ ? require('html-minifier').minify(htmlAfterInjection, this.options.minify === true ? {} : this.options.minify)
412
+ : htmlAfterInjection;
413
+ return Promise.resolve(htmlAfterMinification);
318
414
  }
319
415
 
320
416
  /*
321
417
  * Pushes the content of the given filename to the compilation assets
418
+ * @param {string} filename
419
+ * @param {WebpackCompilation} compilation
420
+ *
421
+ * @returns {string} file basename
322
422
  */
323
423
  addFileToAssets (filename, compilation) {
324
424
  filename = path.resolve(compilation.compiler.context, filename);
@@ -326,73 +426,50 @@ class HtmlWebpackPlugin {
326
426
  fsStatAsync(filename),
327
427
  fsReadFileAsync(filename)
328
428
  ])
329
- .then(([size, source]) => {
330
- return {
331
- size,
332
- source
333
- };
334
- })
335
- .catch(() => Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename)))
336
- .then(results => {
337
- const basename = path.basename(filename);
338
- if (compilation.fileDependencies.add) {
429
+ .then(([size, source]) => {
430
+ return {
431
+ size,
432
+ source
433
+ };
434
+ })
435
+ .catch(() => Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename)))
436
+ .then(results => {
437
+ const basename = path.basename(filename);
339
438
  compilation.fileDependencies.add(filename);
340
- } else {
341
- // Before Webpack 4 - fileDepenencies was an array
342
- compilation.fileDependencies.push(filename);
343
- }
344
- compilation.assets[basename] = {
345
- source: () => results.source,
346
- size: () => results.size.size
347
- };
348
- return basename;
349
- });
439
+ compilation.assets[basename] = {
440
+ source: () => results.source,
441
+ size: () => results.size.size
442
+ };
443
+ return basename;
444
+ });
350
445
  }
351
446
 
352
447
  /**
353
448
  * Helper to sort chunks
449
+ * @param {string[]} entryNames
450
+ * @param {string|((entryNameA: string, entryNameB: string) => number)} sortMode
451
+ * @param {WebpackCompilation} compilation
354
452
  */
355
- sortChunks (chunks, sortMode, chunkGroups) {
356
- // Sort mode auto by default:
357
- if (typeof sortMode === 'undefined') {
358
- sortMode = 'auto';
359
- }
453
+ sortEntryChunks (entryNames, sortMode, compilation) {
360
454
  // Custom function
361
455
  if (typeof sortMode === 'function') {
362
- return chunks.sort(sortMode);
363
- }
364
- // Disabled sorting:
365
- if (sortMode === 'none') {
366
- return chunkSorter.none(chunks);
367
- }
368
- if (sortMode === 'manual') {
369
- return chunkSorter.manual(chunks, this.options.chunks);
456
+ return entryNames.sort(sortMode);
370
457
  }
371
458
  // Check if the given sort mode is a valid chunkSorter sort mode
372
459
  if (typeof chunkSorter[sortMode] !== 'undefined') {
373
- return chunkSorter[sortMode](chunks, chunkGroups);
460
+ return chunkSorter[sortMode](entryNames, compilation, this.options);
374
461
  }
375
462
  throw new Error('"' + sortMode + '" is not a valid chunk sort mode');
376
463
  }
377
464
 
378
465
  /**
379
466
  * Return all chunks from the compilation result which match the exclude and include filters
467
+ * @param {any} chunks
468
+ * @param {string[]|'all'} includedChunks
469
+ * @param {string[]} excludedChunks
380
470
  */
381
471
  filterChunks (chunks, includedChunks, excludedChunks) {
382
- return chunks.filter(chunk => {
383
- const chunkName = chunk.names[0];
384
- // This chunk doesn't have a name. This script can't handled it.
385
- if (chunkName === undefined) {
386
- return false;
387
- }
388
- // Skip if the chunk should be lazy loaded
389
- if (typeof chunk.isInitial === 'function') {
390
- if (!chunk.isInitial()) {
391
- return false;
392
- }
393
- } else if (!chunk.initial) {
394
- return false;
395
- }
472
+ return chunks.filter(chunkName => {
396
473
  // Skip if the chunks should be filtered and the given chunk was not added explicity
397
474
  if (Array.isArray(includedChunks) && includedChunks.indexOf(chunkName) === -1) {
398
475
  return false;
@@ -406,143 +483,316 @@ class HtmlWebpackPlugin {
406
483
  });
407
484
  }
408
485
 
486
+ /**
487
+ * Check if the given asset object consists only of hot-update.js files
488
+ *
489
+ * @param {{
490
+ publicPath: string,
491
+ js: Array<string>,
492
+ css: Array<string>,
493
+ manifest?: string,
494
+ favicon?: string
495
+ }} assets
496
+ */
409
497
  isHotUpdateCompilation (assets) {
410
- return assets.js.length && assets.js.every(name => /\.hot-update\.js$/.test(name));
498
+ return assets.js.length && assets.js.every((assetPath) => /\.hot-update\.js$/.test(assetPath));
411
499
  }
412
500
 
413
- htmlWebpackPluginAssets (compilation, chunks) {
414
- const self = this;
501
+ /**
502
+ * The htmlWebpackPluginAssets extracts the asset information of a webpack compilation
503
+ * for all given entry names
504
+ * @param {WebpackCompilation} compilation
505
+ * @param {string[]} entryNames
506
+ * @returns {{
507
+ publicPath: string,
508
+ js: Array<string>,
509
+ css: Array<string>,
510
+ manifest?: string,
511
+ favicon?: string
512
+ }}
513
+ */
514
+ htmlWebpackPluginAssets (compilation, childCompilationOutputName, entryNames) {
415
515
  const compilationHash = compilation.hash;
416
516
 
417
- // Use the configured public path or build a relative path
517
+ /**
518
+ * @type {string} the configured public path to the asset root
519
+ * if a publicPath is set in the current webpack config use it otherwise
520
+ * fallback to a realtive path
521
+ */
418
522
  let publicPath = typeof compilation.options.output.publicPath !== 'undefined'
419
523
  // If a hard coded public path exists use it
420
524
  ? compilation.mainTemplate.getPublicPath({hash: compilationHash})
421
525
  // If no public path was set get a relative url path
422
- : path.relative(path.resolve(compilation.options.output.path, path.dirname(self.childCompilationOutputName)), compilation.options.output.path)
526
+ : path.relative(path.resolve(compilation.options.output.path, path.dirname(childCompilationOutputName)), compilation.options.output.path)
423
527
  .split(path.sep).join('/');
424
528
 
425
529
  if (publicPath.length && publicPath.substr(-1, 1) !== '/') {
426
530
  publicPath += '/';
427
531
  }
428
532
 
533
+ /**
534
+ * @type {{
535
+ publicPath: string,
536
+ js: Array<string>,
537
+ css: Array<string>,
538
+ manifest?: string,
539
+ favicon?: string
540
+ }}
541
+ */
429
542
  const assets = {
430
543
  // The public path
431
544
  publicPath: publicPath,
432
- // Will contain all js & css files by chunk
433
- chunks: {},
434
545
  // Will contain all js files
435
546
  js: [],
436
547
  // Will contain all css files
437
548
  css: [],
438
549
  // Will contain the html5 appcache manifest files if it exists
439
- manifest: Object.keys(compilation.assets).filter(assetFile => path.extname(assetFile) === '.appcache')[0]
550
+ manifest: Object.keys(compilation.assets).find(assetFile => path.extname(assetFile) === '.appcache'),
551
+ // Favicon
552
+ favicon: undefined
440
553
  };
441
554
 
442
555
  // Append a hash for cache busting
443
- if (this.options.hash) {
444
- assets.manifest = self.appendHash(assets.manifest, compilationHash);
445
- assets.favicon = self.appendHash(assets.favicon, compilationHash);
556
+ if (this.options.hash && assets.manifest) {
557
+ assets.manifest = this.appendHash(assets.manifest, compilationHash);
446
558
  }
447
559
 
448
- for (let i = 0; i < chunks.length; i++) {
449
- const chunk = chunks[i];
450
- const chunkName = chunk.names[0];
451
-
452
- assets.chunks[chunkName] = {};
453
-
454
- // Prepend the public path to all chunk files
455
- let chunkFiles = [].concat(chunk.files).map(chunkFile => publicPath + chunkFile);
456
-
457
- // Append a hash for cache busting
458
- if (this.options.hash) {
459
- chunkFiles = chunkFiles.map(chunkFile => self.appendHash(chunkFile, compilationHash));
460
- }
461
-
462
- // Webpack outputs an array for each chunk when using sourcemaps
463
- // or when one chunk hosts js and css simultaneously
464
- const js = chunkFiles.find(chunkFile => /.js($|\?)/.test(chunkFile));
465
- if (js) {
466
- assets.chunks[chunkName].size = chunk.size;
467
- assets.chunks[chunkName].entry = js;
468
- assets.chunks[chunkName].hash = chunk.hash;
469
- assets.js.push(js);
470
- }
560
+ // Extract paths to .js and .css files from the current compilation
561
+ const entryPointPublicPathMap = {};
562
+ const extensionRegexp = /\.(css|js)(\?|$)/;
563
+ for (let i = 0; i < entryNames.length; i++) {
564
+ const entryName = entryNames[i];
565
+ const entryPointFiles = compilation.entrypoints.get(entryName).getFiles();
566
+ // Prepend the publicPath and append the hash depending on the
567
+ // webpack.output.publicPath and hashOptions
568
+ // E.g. bundle.js -> /bundle.js?hash
569
+ const entryPointPublicPaths = entryPointFiles
570
+ .map(chunkFile => {
571
+ const entryPointPublicPath = publicPath + chunkFile;
572
+ return this.options.hash
573
+ ? this.appendHash(entryPointPublicPath, compilationHash)
574
+ : entryPointPublicPath;
575
+ });
471
576
 
472
- // Gather all css files
473
- const css = chunkFiles.filter(chunkFile => /.css($|\?)/.test(chunkFile));
474
- assets.chunks[chunkName].css = css;
475
- assets.css = assets.css.concat(css);
577
+ entryPointPublicPaths.forEach((entryPointPublicPath) => {
578
+ const extMatch = extensionRegexp.exec(entryPointPublicPath);
579
+ // Skip if the public path is not a .css or .js file
580
+ if (!extMatch) {
581
+ return;
582
+ }
583
+ // Skip if this file is already known
584
+ // (e.g. because of common chunk optimizations)
585
+ if (entryPointPublicPathMap[entryPointPublicPath]) {
586
+ return;
587
+ }
588
+ entryPointPublicPathMap[entryPointPublicPath] = true;
589
+ // ext will contain .js or .css
590
+ const ext = extMatch[1];
591
+ assets[ext].push(entryPointPublicPath);
592
+ });
476
593
  }
594
+ return assets;
595
+ }
477
596
 
478
- // Duplicate css assets can occur on occasion if more than one chunk
479
- // requires the same css.
480
- assets.css = _.uniq(assets.css);
597
+ /**
598
+ * Converts a favicon file from disk to a webpack ressource
599
+ * and returns the url to the ressource
600
+ *
601
+ * @param {string|false} faviconFilePath
602
+ * @param {WebpackCompilation} compilation
603
+ * @parma {string} publicPath
604
+ * @returns {Promise<string|undefined>}
605
+ */
606
+ getFaviconPublicPath (faviconFilePath, compilation, publicPath) {
607
+ if (!faviconFilePath) {
608
+ return Promise.resolve(undefined);
609
+ }
610
+ return this.addFileToAssets(faviconFilePath, compilation)
611
+ .then((faviconName) => {
612
+ const faviconPath = publicPath + faviconName;
613
+ if (this.options.hash) {
614
+ return this.appendHash(faviconPath, compilation.hash);
615
+ }
616
+ return faviconPath;
617
+ });
618
+ }
481
619
 
482
- return assets;
620
+ /**
621
+ * Generate meta tags
622
+ * @returns {HtmlTagObject[]}
623
+ */
624
+ getMetaTags () {
625
+ const metaOptions = this.options.meta;
626
+ if (metaOptions === false) {
627
+ return [];
628
+ }
629
+ // Make tags self-closing in case of xhtml
630
+ // Turn { "viewport" : "width=500, initial-scale=1" } into
631
+ // [{ name:"viewport" content:"width=500, initial-scale=1" }]
632
+ const metaTagAttributeObjects = Object.keys(metaOptions)
633
+ .map((metaName) => {
634
+ const metaTagContent = metaOptions[metaName];
635
+ return (typeof metaTagContent === 'string') ? {
636
+ name: metaName,
637
+ content: metaTagContent
638
+ } : metaTagContent;
639
+ })
640
+ .filter((attribute) => attribute !== false);
641
+ // Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
642
+ // the html-webpack-plugin tag structure
643
+ return metaTagAttributeObjects.map((metaTagAttributes) => {
644
+ if (metaTagAttributes === false) {
645
+ throw new Error('Invalid meta tag');
646
+ }
647
+ return {
648
+ tagName: 'meta',
649
+ voidTag: true,
650
+ attributes: metaTagAttributes
651
+ };
652
+ });
483
653
  }
484
654
 
485
655
  /**
486
- * Injects the assets into the given html string
656
+ * Generate all tags script for the given file paths
657
+ * @param {Array<string>} jsAssets
658
+ * @returns {Array<HtmlTagObject>}
487
659
  */
488
- generateAssetTags (assets) {
489
- // Turn script files into script tags
490
- const scripts = assets.js.map(scriptPath => ({
660
+ generatedScriptTags (jsAssets) {
661
+ return jsAssets.map(scriptAsset => ({
491
662
  tagName: 'script',
492
- closeTag: true,
493
-
663
+ voidTag: false,
494
664
  attributes: {
495
- type: 'text/javascript',
496
- src: scriptPath
665
+ src: scriptAsset
497
666
  }
498
667
  }));
499
- // Make tags self-closing in case of xhtml
500
- const selfClosingTag = !!this.options.xhtml;
501
- // Turn css files into link tags
502
- const styles = assets.css.map(stylePath => ({
503
- tagName: 'link',
504
- selfClosingTag: selfClosingTag,
668
+ }
505
669
 
670
+ /**
671
+ * Generate all style tags for the given file paths
672
+ * @param {Array<string>} cssAssets
673
+ * @returns {Array<HtmlTagObject>}
674
+ */
675
+ generateStyleTags (cssAssets) {
676
+ return cssAssets.map(styleAsset => ({
677
+ tagName: 'link',
678
+ voidTag: true,
506
679
  attributes: {
507
- href: stylePath,
680
+ href: styleAsset,
508
681
  rel: 'stylesheet'
509
682
  }
510
683
  }));
511
- // Injection targets
512
- let head = [];
513
- let body = [];
514
-
515
- // If there is a favicon present, add it to the head
516
- if (assets.favicon) {
517
- head.push({
518
- tagName: 'link',
519
- selfClosingTag: selfClosingTag,
520
- attributes: {
521
- rel: 'shortcut icon',
522
- href: assets.favicon
523
- }
524
- });
684
+ }
685
+
686
+ /**
687
+ * Generate all meta tags for the given meta configuration
688
+ * @param {false | {
689
+ [name: string]: string|false // name content pair e.g. {viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no'}`
690
+ | {[attributeName: string]: string|boolean} // custom properties e.g. { name:"viewport" content:"width=500, initial-scale=1" }
691
+ }} metaOptions
692
+ * @returns {Array<HtmlTagObject>}
693
+ */
694
+ generatedMetaTags (metaOptions) {
695
+ if (metaOptions === false) {
696
+ return [];
525
697
  }
526
- // Add styles to the head
527
- head = head.concat(styles);
528
- // Add scripts to body or head
529
- if (this.options.inject === 'head') {
530
- head = head.concat(scripts);
698
+ // Make tags self-closing in case of xhtml
699
+ // Turn { "viewport" : "width=500, initial-scale=1" } into
700
+ // [{ name:"viewport" content:"width=500, initial-scale=1" }]
701
+ const metaTagAttributeObjects = Object.keys(metaOptions)
702
+ .map((metaName) => {
703
+ const metaTagContent = metaOptions[metaName];
704
+ return (typeof metaTagContent === 'string') ? {
705
+ name: metaName,
706
+ content: metaTagContent
707
+ } : metaTagContent;
708
+ })
709
+ .filter((attribute) => attribute !== false);
710
+ // Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
711
+ // the html-webpack-plugin tag structure
712
+ return metaTagAttributeObjects.map((metaTagAttributes) => {
713
+ if (metaTagAttributes === false) {
714
+ throw new Error('Invalid meta tag');
715
+ }
716
+ return {
717
+ tagName: 'meta',
718
+ voidTag: true,
719
+ attributes: metaTagAttributes
720
+ };
721
+ });
722
+ }
723
+
724
+ /**
725
+ * Generate a favicon tag for the given file path
726
+ * @param {string| undefined} faviconPath
727
+ * @returns {Array<HtmlTagObject>}
728
+ */
729
+ generateFaviconTags (faviconPath) {
730
+ if (!faviconPath) {
731
+ return [];
732
+ }
733
+ return [{
734
+ tagName: 'link',
735
+ voidTag: true,
736
+ attributes: {
737
+ rel: 'shortcut icon',
738
+ href: faviconPath
739
+ }
740
+ }];
741
+ }
742
+
743
+ /**
744
+ * Group assets to head and bottom tags
745
+ *
746
+ * @param {{
747
+ scripts: Array<HtmlTagObject>;
748
+ styles: Array<HtmlTagObject>;
749
+ meta: Array<HtmlTagObject>;
750
+ }} assetTags
751
+ * @param {"body" | "head"} scriptTarget
752
+ * @returns {{
753
+ headTags: Array<HtmlTagObject>;
754
+ bodyTags: Array<HtmlTagObject>;
755
+ }}
756
+ */
757
+ generateAssetGroups (assetTags, scriptTarget) {
758
+ /** @type {{ headTags: Array<HtmlTagObject>; bodyTags: Array<HtmlTagObject>; }} */
759
+ const result = {
760
+ headTags: [
761
+ ...assetTags.meta,
762
+ ...assetTags.styles
763
+ ],
764
+ bodyTags: []
765
+ };
766
+ // Add script tags to head or body depending on
767
+ // the htmlPluginOptions
768
+ if (scriptTarget === 'body') {
769
+ result.bodyTags.push(...assetTags.scripts);
531
770
  } else {
532
- body = body.concat(scripts);
771
+ result.headTags.push(...assetTags.scripts);
533
772
  }
534
- return {head: head, body: body};
773
+ return result;
535
774
  }
536
775
 
537
776
  /**
538
777
  * Injects the assets into the given html string
778
+ *
779
+ * @param {string} html
780
+ * The input html
781
+ * @param {any} assets
782
+ * @param {{
783
+ headTags: HtmlTagObject[],
784
+ bodyTags: HtmlTagObject[]
785
+ }} assetTags
786
+ * The asset tags to inject
787
+ *
788
+ * @returns {string}
539
789
  */
540
790
  injectAssetsIntoHtml (html, assets, assetTags) {
541
791
  const htmlRegExp = /(<html[^>]*>)/i;
542
792
  const headRegExp = /(<\/head\s*>)/i;
543
793
  const bodyRegExp = /(<\/body\s*>)/i;
544
- const body = assetTags.body.map(this.createHtmlTag);
545
- const head = assetTags.head.map(this.createHtmlTag);
794
+ const body = assetTags.bodyTags.map((assetTagObject) => htmlTagObjectToString(assetTagObject, this.options.xhtml));
795
+ const head = assetTags.headTags.map((assetTagObject) => htmlTagObjectToString(assetTagObject, this.options.xhtml));
546
796
 
547
797
  if (body.length) {
548
798
  if (bodyRegExp.test(html)) {
@@ -582,7 +832,10 @@ class HtmlWebpackPlugin {
582
832
  }
583
833
 
584
834
  /**
585
- * Appends a cache busting hash
835
+ * Appends a cache busting hash to the query string of the url
836
+ * E.g. http://localhost:8080/ -> http://localhost:8080/?50c9096ba6183fd728eeb065a26ec175
837
+ * @param {string} url
838
+ * @param {string} hash
586
839
  */
587
840
  appendHash (url, hash) {
588
841
  if (!url) {
@@ -591,28 +844,12 @@ class HtmlWebpackPlugin {
591
844
  return url + (url.indexOf('?') === -1 ? '?' : '&') + hash;
592
845
  }
593
846
 
594
- /**
595
- * Turn a tag definition into a html string
596
- */
597
- createHtmlTag (tagDefinition) {
598
- const attributes = Object.keys(tagDefinition.attributes || {})
599
- .filter(attributeName => tagDefinition.attributes[attributeName] !== false)
600
- .map(attributeName => {
601
- if (tagDefinition.attributes[attributeName] === true) {
602
- return attributeName;
603
- }
604
- return attributeName + '="' + tagDefinition.attributes[attributeName] + '"';
605
- });
606
- // Backport of 3.x void tag definition
607
- const voidTag = tagDefinition.voidTag !== undefined ? tagDefinition.voidTag : !tagDefinition.closeTag;
608
- const selfClosingTag = tagDefinition.voidTag !== undefined ? tagDefinition.voidTag && this.options.xhtml : tagDefinition.selfClosingTag;
609
- return '<' + [tagDefinition.tagName].concat(attributes).join(' ') + (selfClosingTag ? '/' : '') + '>' +
610
- (tagDefinition.innerHTML || '') +
611
- (voidTag ? '' : '</' + tagDefinition.tagName + '>');
612
- }
613
-
614
847
  /**
615
848
  * Helper to return the absolute template path with a fallback loader
849
+ * @param {string} template
850
+ * The path to the tempalate e.g. './index.html'
851
+ * @param {string} context
852
+ * The webpack base resolution path for relative paths e.g. process.cwd()
616
853
  */
617
854
  getFullTemplatePath (template, context) {
618
855
  // If the template doesn't use a loader use the lodash template loader
@@ -634,75 +871,59 @@ class HtmlWebpackPlugin {
634
871
  files.sort();
635
872
  return files;
636
873
  }
637
-
638
- /**
639
- * Helper to promisify compilation.applyPluginsAsyncWaterfall that returns
640
- * a function that helps to merge given plugin arguments with processed ones
641
- */
642
- applyPluginsAsyncWaterfall (compilation) {
643
- if (compilation.hooks) {
644
- return (eventName, requiresResult, pluginArgs) => {
645
- const ccEventName = trainCaseToCamelCase(eventName);
646
- if (!compilation.hooks[ccEventName]) {
647
- compilation.errors.push(
648
- new Error('No hook found for ' + eventName)
649
- );
650
- }
651
-
652
- return compilation.hooks[ccEventName].promise(pluginArgs);
653
- };
654
- }
655
-
656
- // Before Webpack 4
657
- const promisedApplyPluginsAsyncWaterfall = function (name, init) {
658
- return new Promise((resolve, reject) => {
659
- const callback = function (err, result) {
660
- if (err) {
661
- return reject(err);
662
- }
663
- resolve(result);
664
- };
665
- compilation.applyPluginsAsyncWaterfall(name, init, callback);
666
- });
667
- };
668
-
669
- return (eventName, requiresResult, pluginArgs) => promisedApplyPluginsAsyncWaterfall(eventName, pluginArgs)
670
- .then(result => {
671
- if (requiresResult && !result) {
672
- compilation.warnings.push(
673
- new Error('Using ' + eventName + ' without returning a result is deprecated.')
674
- );
675
- }
676
- return _.extend(pluginArgs, result);
677
- });
678
- }
679
- }
680
-
681
- /**
682
- * Takes a string in train case and transforms it to camel case
683
- *
684
- * Example: 'hello-my-world' to 'helloMyWorld'
685
- *
686
- * @param {string} word
687
- */
688
- function trainCaseToCamelCase (word) {
689
- return word.replace(/-([\w])/g, (match, p1) => p1.toUpperCase());
690
874
  }
691
875
 
692
876
  /**
693
877
  * The default for options.templateParameter
694
878
  * Generate the template parameters
879
+ *
880
+ * Generate the template parameters for the template function
881
+ * @param {WebpackCompilation} compilation
882
+ * @param {{
883
+ publicPath: string,
884
+ js: Array<string>,
885
+ css: Array<string>,
886
+ manifest?: string,
887
+ favicon?: string
888
+ }} assets
889
+ * @param {{
890
+ headTags: HtmlTagObject[],
891
+ bodyTags: HtmlTagObject[]
892
+ }} assetTags
893
+ * @param {HtmlWebpackPluginOptions} options
894
+ * @returns {HtmlWebpackPluginTemplateParameter}
695
895
  */
696
- function templateParametersGenerator (compilation, assets, options) {
896
+ function templateParametersGenerator (compilation, assets, assetTags, options) {
897
+ const xhtml = options.xhtml;
898
+ assetTags.headTags.toString = function () {
899
+ return this.map((assetTagObject) => htmlTagObjectToString(assetTagObject, xhtml)).join('');
900
+ };
901
+ assetTags.bodyTags.toString = function () {
902
+ return this.map((assetTagObject) => htmlTagObjectToString(assetTagObject, xhtml)).join('');
903
+ };
697
904
  return {
698
905
  compilation: compilation,
699
- webpack: compilation.getStats().toJson(),
700
906
  webpackConfig: compilation.options,
701
907
  htmlWebpackPlugin: {
908
+ tags: assetTags,
702
909
  files: assets,
703
910
  options: options
704
911
  }
705
912
  };
706
913
  }
707
914
 
915
+ // Statics:
916
+ /**
917
+ * The major version number of this plugin
918
+ */
919
+ HtmlWebpackPlugin.version = 4;
920
+
921
+ /**
922
+ * A static helper to get the hooks for this plugin
923
+ *
924
+ * Usage: HtmlWebpackPlugin.getHooks(compilation).HOOK_NAME.tapAsync('YourPluginName', () => { ... });
925
+ */
926
+ HtmlWebpackPlugin.getHooks = getHtmlWebpackPluginHooks;
927
+ HtmlWebpackPlugin.createHtmlTagObject = createHtmlTagObject;
928
+
708
929
  module.exports = HtmlWebpackPlugin;