html-webpack-plugin 3.2.0 → 4.0.0-alpha

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,24 +1,44 @@
1
+ // @ts-check
1
2
  'use strict';
2
3
 
3
4
  // use Polyfill for util.promisify in node versions < v8
4
5
  const promisify = require('util.promisify');
5
6
 
7
+ // Import types
8
+ /* eslint-disable */
9
+ /// <reference path="./typings.d.ts" />
10
+ /* eslint-enable */
11
+ /** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */
12
+ /** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
13
+
6
14
  const vm = require('vm');
7
15
  const fs = require('fs');
8
16
  const _ = require('lodash');
9
17
  const path = require('path');
18
+
19
+ const htmlTagObjectToString = require('./lib/html-tags').htmlTagObjectToString;
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;
25
+ const getHtmlWebpackPluginHook = require('./lib/hooks.js').getHtmlWebpackPluginHook;
13
26
 
14
27
  const fsStatAsync = promisify(fs.stat);
15
28
  const fsReadFileAsync = promisify(fs.readFile);
16
29
 
17
30
  class HtmlWebpackPlugin {
31
+ /**
32
+ * @param {Partial<HtmlWebpackPluginOptions>} options
33
+ */
18
34
  constructor (options) {
19
35
  // Default options
20
- this.options = _.extend({
36
+ /**
37
+ * @type {HtmlWebpackPluginOptions}
38
+ */
39
+ this.options = Object.assign({
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,
@@ -35,8 +55,22 @@ class HtmlWebpackPlugin {
35
55
  title: 'Webpack App',
36
56
  xhtml: false
37
57
  }, options);
58
+ // Instance variables to keep caching information
59
+ // for multiple builds
60
+ this.childCompilerHash = undefined;
61
+ this.childCompilationOutputName = undefined;
62
+ this.assetJson = undefined;
63
+ this.hash = undefined;
64
+ /**
65
+ * The major version number of this plugin
66
+ */
67
+ this.version = 4;
38
68
  }
39
69
 
70
+ /**
71
+ * apply is called by the webpack main compiler during the start phase
72
+ * @param {WebpackCompiler} compiler
73
+ */
40
74
  apply (compiler) {
41
75
  const self = this;
42
76
  let isCompilationCached = false;
@@ -51,22 +85,10 @@ class HtmlWebpackPlugin {
51
85
  this.options.filename = path.relative(compiler.options.output.path, filename);
52
86
  }
53
87
 
54
- // setup hooks for webpack 4
55
- if (compiler.hooks) {
56
- compiler.hooks.compilation.tap('HtmlWebpackPluginHooks', compilation => {
57
- const SyncWaterfallHook = require('tapable').SyncWaterfallHook;
58
- const AsyncSeriesWaterfallHook = require('tapable').AsyncSeriesWaterfallHook;
59
- compilation.hooks.htmlWebpackPluginAlterChunks = new SyncWaterfallHook(['chunks', 'objectWithPluginRef']);
60
- compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration = new AsyncSeriesWaterfallHook(['pluginArgs']);
61
- compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing = new AsyncSeriesWaterfallHook(['pluginArgs']);
62
- compilation.hooks.htmlWebpackPluginAlterAssetTags = new AsyncSeriesWaterfallHook(['pluginArgs']);
63
- compilation.hooks.htmlWebpackPluginAfterHtmlProcessing = new AsyncSeriesWaterfallHook(['pluginArgs']);
64
- compilation.hooks.htmlWebpackPluginAfterEmit = new AsyncSeriesWaterfallHook(['pluginArgs']);
65
- });
66
- }
88
+ // setup hooks for third party plugins
89
+ compiler.hooks.compilation.tap('HtmlWebpackPluginHooks', getHtmlWebpackPluginHooks);
67
90
 
68
- // Backwards compatible version of: compiler.hooks.make.tapAsync()
69
- (compiler.hooks ? compiler.hooks.make.tapAsync.bind(compiler.hooks.make, 'HtmlWebpackPlugin') : compiler.plugin.bind(compiler, 'make'))((compilation, callback) => {
91
+ compiler.hooks.make.tapAsync('HtmlWebpackPlugin', (compilation, callback) => {
70
92
  // Compile the template (queued)
71
93
  compilationPromise = childCompiler.compileTemplate(self.options.template, compiler.context, self.options.filename, compilation)
72
94
  .catch(err => {
@@ -86,144 +108,124 @@ class HtmlWebpackPlugin {
86
108
  });
87
109
  });
88
110
 
89
- // Backwards compatible version of: compiler.plugin.emit.tapAsync()
90
- (compiler.hooks ? compiler.hooks.emit.tapAsync.bind(compiler.hooks.emit, 'HtmlWebpackPlugin') : compiler.plugin.bind(compiler, 'emit'))((compilation, callback) => {
91
- const applyPluginsAsyncWaterfall = self.applyPluginsAsyncWaterfall(compilation);
92
- // Get chunks info as json
93
- // Note: we're excluding stuff that we don't need to improve toJson serialization speed.
94
- const chunkOnlyConfig = {
95
- assets: false,
96
- cached: false,
97
- children: false,
98
- chunks: true,
99
- chunkModules: false,
100
- chunkOrigins: false,
101
- errorDetails: false,
102
- hash: false,
103
- modules: false,
104
- reasons: false,
105
- source: false,
106
- timings: false,
107
- version: false
108
- };
109
- const allChunks = compilation.getStats().toJson(chunkOnlyConfig).chunks;
110
- // Filter chunks (options.chunks and options.excludeCHunks)
111
- let chunks = self.filterChunks(allChunks, self.options.chunks, self.options.excludeChunks);
112
- // Sort chunks
113
- chunks = self.sortChunks(chunks, self.options.chunksSortMode, compilation);
114
- // Let plugins alter the chunks and the chunk sorting
115
- if (compilation.hooks) {
116
- chunks = compilation.hooks.htmlWebpackPluginAlterChunks.call(chunks, { plugin: self });
117
- } else {
118
- // Before Webpack 4
119
- chunks = compilation.applyPluginsWaterfall('html-webpack-plugin-alter-chunks', chunks, { plugin: self });
120
- }
121
- // Get assets
122
- const assets = self.htmlWebpackPluginAssets(compilation, chunks);
123
- // If this is a hot update compilation, move on!
124
- // This solves a problem where an `index.html` file is generated for hot-update js files
125
- // It only happens in Webpack 2, where hot updates are emitted separately before the full bundle
126
- if (self.isHotUpdateCompilation(assets)) {
127
- return callback();
128
- }
111
+ compiler.hooks.emit.tapAsync('HtmlWebpackPlugin',
112
+ /**
113
+ * Hook into the webpack emit phase
114
+ * @param {WebpackCompilation} compilation
115
+ * @param {() => void} callback
116
+ */
117
+ (compilation, callback) => {
118
+ // Get all entry point names for this html file
119
+ const entryNames = Array.from(compilation.entrypoints.keys());
120
+ const filteredEntryNames = self.filterChunks(entryNames, self.options.chunks, self.options.excludeChunks);
121
+ const sortedEntryNames = self.sortEntryChunks(filteredEntryNames, this.options.chunksSortMode, compilation);
122
+ // Turn the entry point names into file paths
123
+ const assets = self.htmlWebpackPluginAssets(compilation, sortedEntryNames);
124
+
125
+ // If this is a hot update compilation, move on!
126
+ // This solves a problem where an `index.html` file is generated for hot-update js files
127
+ // It only happens in Webpack 2, where hot updates are emitted separately before the full bundle
128
+ if (self.isHotUpdateCompilation(assets)) {
129
+ return callback();
130
+ }
129
131
 
130
- // If the template and the assets did not change we don't have to emit the html
131
- const assetJson = JSON.stringify(self.getAssetFiles(assets));
132
- if (isCompilationCached && self.options.cache && assetJson === self.assetJson) {
133
- return callback();
134
- } else {
135
- self.assetJson = assetJson;
136
- }
132
+ // If the template and the assets did not change we don't have to emit the html
133
+ const assetJson = JSON.stringify(self.getAssetFiles(assets));
134
+ if (isCompilationCached && self.options.cache && assetJson === self.assetJson) {
135
+ return callback();
136
+ } else {
137
+ self.assetJson = assetJson;
138
+ }
137
139
 
138
- Promise.resolve()
140
+ Promise.resolve()
139
141
  // Favicon
140
- .then(() => {
141
- if (self.options.favicon) {
142
- return self.addFileToAssets(self.options.favicon, compilation)
143
- .then(faviconBasename => {
144
- let publicPath = compilation.mainTemplate.getPublicPath({hash: compilation.hash}) || '';
145
- if (publicPath && publicPath.substr(-1) !== '/') {
146
- publicPath += '/';
147
- }
148
- assets.favicon = publicPath + faviconBasename;
149
- });
150
- }
151
- })
142
+ .then(() => {
143
+ if (self.options.favicon) {
144
+ return self.addFileToAssets(self.options.favicon, compilation)
145
+ .then(faviconBasename => {
146
+ let publicPath = compilation.mainTemplate.getPublicPath({hash: compilation.hash}) || '';
147
+ if (publicPath && publicPath.substr(-1) !== '/') {
148
+ publicPath += '/';
149
+ }
150
+ assets.favicon = publicPath + faviconBasename;
151
+ });
152
+ }
153
+ })
152
154
  // Wait for the compilation to finish
153
- .then(() => compilationPromise)
154
- .then(compiledTemplate => {
155
+ .then(() => compilationPromise)
156
+ .then(compiledTemplate => {
155
157
  // Allow to use a custom function / string instead
156
- if (self.options.templateContent !== undefined) {
157
- return self.options.templateContent;
158
- }
159
- // Once everything is compiled evaluate the html factory
160
- // and replace it with its content
161
- return self.evaluateCompilationResult(compilation, compiledTemplate);
162
- })
158
+ if (self.options.templateContent !== false) {
159
+ return self.options.templateContent;
160
+ }
161
+ // Once everything is compiled evaluate the html factory
162
+ // and replace it with its content
163
+ return self.evaluateCompilationResult(compilation, compiledTemplate);
164
+ })
163
165
  // Allow plugins to make changes to the assets before invoking the template
164
166
  // This only makes sense to use if `inject` is `false`
165
- .then(compilationResult => applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-generation', false, {
166
- assets: assets,
167
- outputName: self.childCompilationOutputName,
168
- plugin: self
169
- })
170
- .then(() => compilationResult))
167
+ .then(compilationResult => getHtmlWebpackPluginHook(compilation, 'htmlWebpackPluginBeforeHtmlGeneration').promise({
168
+ assets: assets,
169
+ outputName: self.childCompilationOutputName,
170
+ plugin: self
171
+ })
172
+ .then(() => compilationResult))
171
173
  // Execute the template
172
- .then(compilationResult => typeof compilationResult !== 'function'
173
- ? compilationResult
174
- : self.executeTemplate(compilationResult, chunks, assets, compilation))
174
+ .then(compilationResult => typeof compilationResult !== 'function'
175
+ ? compilationResult
176
+ : self.executeTemplate(compilationResult, assets, compilation))
175
177
  // Allow plugins to change the html before assets are injected
176
- .then(html => {
177
- const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
178
- return applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-processing', true, pluginArgs);
179
- })
180
- .then(result => {
181
- const html = result.html;
182
- const assets = result.assets;
183
- // Prepare script and link tags
184
- const assetTags = self.generateHtmlTags(assets);
185
- const pluginArgs = {head: assetTags.head, body: assetTags.body, plugin: self, chunks: chunks, outputName: self.childCompilationOutputName};
186
- // Allow plugins to change the assetTag definitions
187
- return applyPluginsAsyncWaterfall('html-webpack-plugin-alter-asset-tags', true, pluginArgs)
188
- .then(result => self.postProcessHtml(html, assets, { body: result.body, head: result.head })
189
- .then(html => _.extend(result, {html: html, assets: assets})));
190
- })
178
+ .then(html => {
179
+ const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
180
+ return getHtmlWebpackPluginHook(compilation, 'htmlWebpackPluginBeforeHtmlProcessing').promise(pluginArgs);
181
+ })
182
+ .then(result => {
183
+ const html = result.html;
184
+ const assets = result.assets;
185
+ // Prepare script and link tags
186
+ const assetTags = self.generateHtmlTagObjects(assets);
187
+ const pluginArgs = {head: assetTags.head, body: assetTags.body, plugin: self, outputName: self.childCompilationOutputName};
188
+ // Allow plugins to change the assetTag definitions
189
+ return getHtmlWebpackPluginHook(compilation, 'htmlWebpackPluginAlterAssetTags').promise(pluginArgs)
190
+ .then(result => self.postProcessHtml(html, assets, { body: result.body, head: result.head })
191
+ .then(html => _.extend(result, {html: html, assets: assets})));
192
+ })
191
193
  // Allow plugins to change the html after assets are injected
192
- .then(result => {
193
- const html = result.html;
194
- const assets = result.assets;
195
- const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
196
- return applyPluginsAsyncWaterfall('html-webpack-plugin-after-html-processing', true, pluginArgs)
197
- .then(result => result.html);
198
- })
199
- .catch(err => {
194
+ .then(result => {
195
+ const html = result.html;
196
+ const assets = result.assets;
197
+ const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
198
+ return getHtmlWebpackPluginHook(compilation, 'htmlWebpackPluginAfterHtmlProcessing').promise(pluginArgs)
199
+ .then(result => result.html);
200
+ })
201
+ .catch(err => {
200
202
  // In case anything went wrong the promise is resolved
201
203
  // with the error message and an error is logged
202
- compilation.errors.push(prettyError(err, compiler.context).toString());
203
- // Prevent caching
204
- self.hash = null;
205
- return self.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
206
- })
207
- .then(html => {
204
+ compilation.errors.push(prettyError(err, compiler.context).toString());
205
+ // Prevent caching
206
+ self.hash = null;
207
+ return self.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
208
+ })
209
+ .then(html => {
208
210
  // Replace the compilation result with the evaluated html code
209
- compilation.assets[self.childCompilationOutputName] = {
210
- source: () => html,
211
- size: () => html.length
212
- };
213
- })
214
- .then(() => applyPluginsAsyncWaterfall('html-webpack-plugin-after-emit', false, {
215
- html: compilation.assets[self.childCompilationOutputName],
216
- outputName: self.childCompilationOutputName,
217
- plugin: self
218
- }).catch(err => {
219
- console.error(err);
220
- return null;
221
- }).then(() => null))
211
+ compilation.assets[self.childCompilationOutputName] = {
212
+ source: () => html,
213
+ size: () => html.length
214
+ };
215
+ })
216
+ .then(() => getHtmlWebpackPluginHook(compilation, 'htmlWebpackPluginAfterEmit').promise({
217
+ html: compilation.assets[self.childCompilationOutputName],
218
+ outputName: self.childCompilationOutputName,
219
+ plugin: self
220
+ }).catch(err => {
221
+ console.error(err);
222
+ return null;
223
+ }).then(() => null))
222
224
  // Let webpack continue with it
223
- .then(() => {
224
- callback();
225
- });
226
- });
225
+ .then(() => {
226
+ callback();
227
+ });
228
+ });
227
229
  }
228
230
 
229
231
  /**
@@ -258,6 +260,8 @@ class HtmlWebpackPlugin {
258
260
 
259
261
  /**
260
262
  * Generate the template parameters for the template function
263
+ * @param {WebpackCompilation} compilation
264
+ *
261
265
  */
262
266
  getTemplateParameters (compilation, assets) {
263
267
  if (typeof this.options.templateParameters === 'function') {
@@ -272,22 +276,21 @@ class HtmlWebpackPlugin {
272
276
  /**
273
277
  * Html post processing
274
278
  *
275
- * Returns a promise
279
+ * @returns Promise<string>
276
280
  */
277
- executeTemplate (templateFunction, chunks, assets, compilation) {
278
- return Promise.resolve()
279
- // Template processing
280
- .then(() => {
281
- const templateParams = this.getTemplateParameters(compilation, assets);
282
- let html = '';
283
- try {
284
- html = templateFunction(templateParams);
285
- } catch (e) {
286
- compilation.errors.push(new Error('Template execution failed: ' + e));
287
- return Promise.reject(e);
288
- }
289
- return html;
290
- });
281
+ executeTemplate (templateFunction, assets, compilation) {
282
+ // Template processing
283
+ const templateParams = this.getTemplateParameters(compilation, assets);
284
+ let html = '';
285
+ try {
286
+ html = templateFunction(templateParams);
287
+ } catch (e) {
288
+ compilation.errors.push(new Error('Template execution failed: ' + e));
289
+ return Promise.reject(e);
290
+ }
291
+ // If html is a promise return the promise
292
+ // If html is a string turn it into a promise
293
+ return Promise.resolve().then(() => html);
291
294
  }
292
295
 
293
296
  /**
@@ -296,24 +299,23 @@ class HtmlWebpackPlugin {
296
299
  * Returns a promise
297
300
  */
298
301
  postProcessHtml (html, assets, assetTags) {
299
- const self = this;
300
302
  if (typeof html !== 'string') {
301
303
  return Promise.reject('Expected html to be a string but got ' + JSON.stringify(html));
302
304
  }
303
305
  return Promise.resolve()
304
306
  // Inject
305
307
  .then(() => {
306
- if (self.options.inject) {
307
- return self.injectAssetsIntoHtml(html, assets, assetTags);
308
+ if (this.options.inject) {
309
+ return this.injectAssetsIntoHtml(html, assets, assetTags);
308
310
  } else {
309
311
  return html;
310
312
  }
311
313
  })
312
314
  // Minify
313
315
  .then(html => {
314
- if (self.options.minify) {
316
+ if (this.options.minify) {
315
317
  const minify = require('html-minifier').minify;
316
- return minify(html, self.options.minify);
318
+ return minify(html, this.options.minify === true ? {} : this.options.minify);
317
319
  }
318
320
  return html;
319
321
  });
@@ -321,6 +323,8 @@ class HtmlWebpackPlugin {
321
323
 
322
324
  /*
323
325
  * Pushes the content of the given filename to the compilation assets
326
+ * @param {string} filename
327
+ * @param {WebpackCompilation} compilation
324
328
  */
325
329
  addFileToAssets (filename, compilation) {
326
330
  filename = path.resolve(compilation.compiler.context, filename);
@@ -328,62 +332,50 @@ class HtmlWebpackPlugin {
328
332
  fsStatAsync(filename),
329
333
  fsReadFileAsync(filename)
330
334
  ])
331
- .then(([size, source]) => {
332
- return {
333
- size,
334
- source
335
- };
336
- })
337
- .catch(() => Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename)))
338
- .then(results => {
339
- const basename = path.basename(filename);
340
- if (compilation.fileDependencies.add) {
335
+ .then(([size, source]) => {
336
+ return {
337
+ size,
338
+ source
339
+ };
340
+ })
341
+ .catch(() => Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename)))
342
+ .then(results => {
343
+ const basename = path.basename(filename);
341
344
  compilation.fileDependencies.add(filename);
342
- } else {
343
- // Before Webpack 4 - fileDepenencies was an array
344
- compilation.fileDependencies.push(filename);
345
- }
346
- compilation.assets[basename] = {
347
- source: () => results.source,
348
- size: () => results.size.size
349
- };
350
- return basename;
351
- });
345
+ compilation.assets[basename] = {
346
+ source: () => results.source,
347
+ size: () => results.size.size
348
+ };
349
+ return basename;
350
+ });
352
351
  }
353
352
 
354
353
  /**
355
354
  * Helper to sort chunks
355
+ * @param {string[]} entryNames
356
+ * @param {string|((entryNameA: string, entryNameB: string) => number)} sortMode
357
+ * @param {WebpackCompilation} compilation
356
358
  */
357
- sortChunks (chunks, sortMode, compilation) {
359
+ sortEntryChunks (entryNames, sortMode, compilation) {
358
360
  // Custom function
359
361
  if (typeof sortMode === 'function') {
360
- return chunks.sort(sortMode);
362
+ return entryNames.sort(sortMode);
361
363
  }
362
364
  // Check if the given sort mode is a valid chunkSorter sort mode
363
365
  if (typeof chunkSorter[sortMode] !== 'undefined') {
364
- return chunkSorter[sortMode](chunks, this.options, compilation);
366
+ return chunkSorter[sortMode](entryNames, compilation, this.options);
365
367
  }
366
368
  throw new Error('"' + sortMode + '" is not a valid chunk sort mode');
367
369
  }
368
370
 
369
371
  /**
370
372
  * Return all chunks from the compilation result which match the exclude and include filters
373
+ * @param {any} chunks
374
+ * @param {string[]|'all'} includedChunks
375
+ * @param {string[]} excludedChunks
371
376
  */
372
377
  filterChunks (chunks, includedChunks, excludedChunks) {
373
- return chunks.filter(chunk => {
374
- const chunkName = chunk.names[0];
375
- // This chunk doesn't have a name. This script can't handled it.
376
- if (chunkName === undefined) {
377
- return false;
378
- }
379
- // Skip if the chunk should be lazy loaded
380
- if (typeof chunk.isInitial === 'function') {
381
- if (!chunk.isInitial()) {
382
- return false;
383
- }
384
- } else if (!chunk.initial) {
385
- return false;
386
- }
378
+ return chunks.filter(chunkName => {
387
379
  // Skip if the chunks should be filtered and the given chunk was not added explicity
388
380
  if (Array.isArray(includedChunks) && includedChunks.indexOf(chunkName) === -1) {
389
381
  return false;
@@ -401,95 +393,116 @@ class HtmlWebpackPlugin {
401
393
  return assets.js.length && assets.js.every(name => /\.hot-update\.js$/.test(name));
402
394
  }
403
395
 
404
- htmlWebpackPluginAssets (compilation, chunks) {
405
- const self = this;
396
+ /**
397
+ * The htmlWebpackPluginAssets extracts the asset information of a webpack compilation
398
+ * for all given entry names
399
+ * @param {WebpackCompilation} compilation
400
+ * @param {string[]} entryNames
401
+ * @returns {{
402
+ publicPath: string,
403
+ js: Array<{entryName: string, path: string}>,
404
+ css: Array<{entryName: string, path: string}>,
405
+ manifest?: string,
406
+ favicon?: string
407
+ }}
408
+ */
409
+ htmlWebpackPluginAssets (compilation, entryNames) {
406
410
  const compilationHash = compilation.hash;
407
411
 
408
- // Use the configured public path or build a relative path
412
+ /**
413
+ * @type {string} the configured public path to the asset root
414
+ * if a publicPath is set in the current webpack config use it otherwise
415
+ * fallback to a realtive path
416
+ */
409
417
  let publicPath = typeof compilation.options.output.publicPath !== 'undefined'
410
418
  // If a hard coded public path exists use it
411
419
  ? compilation.mainTemplate.getPublicPath({hash: compilationHash})
412
420
  // If no public path was set get a relative url path
413
- : path.relative(path.resolve(compilation.options.output.path, path.dirname(self.childCompilationOutputName)), compilation.options.output.path)
421
+ : path.relative(path.resolve(compilation.options.output.path, path.dirname(this.childCompilationOutputName)), compilation.options.output.path)
414
422
  .split(path.sep).join('/');
415
423
 
416
424
  if (publicPath.length && publicPath.substr(-1, 1) !== '/') {
417
425
  publicPath += '/';
418
426
  }
419
427
 
428
+ /**
429
+ * @type {{
430
+ publicPath: string,
431
+ js: Array<{entryName: string, path: string}>,
432
+ css: Array<{entryName: string, path: string}>,
433
+ manifest?: string,
434
+ favicon?: string
435
+ }}
436
+ */
420
437
  const assets = {
421
438
  // The public path
422
439
  publicPath: publicPath,
423
- // Will contain all js & css files by chunk
424
- chunks: {},
425
440
  // Will contain all js files
426
441
  js: [],
427
442
  // Will contain all css files
428
443
  css: [],
429
444
  // Will contain the html5 appcache manifest files if it exists
430
- manifest: Object.keys(compilation.assets).filter(assetFile => path.extname(assetFile) === '.appcache')[0]
445
+ manifest: Object.keys(compilation.assets).find(assetFile => path.extname(assetFile) === '.appcache'),
446
+ // Favicon
447
+ favicon: undefined
431
448
  };
432
449
 
433
450
  // Append a hash for cache busting
434
- if (this.options.hash) {
435
- assets.manifest = self.appendHash(assets.manifest, compilationHash);
436
- assets.favicon = self.appendHash(assets.favicon, compilationHash);
451
+ if (this.options.hash && assets.manifest) {
452
+ assets.manifest = this.appendHash(assets.manifest, compilationHash);
437
453
  }
438
454
 
439
- for (let i = 0; i < chunks.length; i++) {
440
- const chunk = chunks[i];
441
- const chunkName = chunk.names[0];
442
-
443
- assets.chunks[chunkName] = {};
444
-
445
- // Prepend the public path to all chunk files
446
- let chunkFiles = [].concat(chunk.files).map(chunkFile => publicPath + chunkFile);
447
-
448
- // Append a hash for cache busting
449
- if (this.options.hash) {
450
- chunkFiles = chunkFiles.map(chunkFile => self.appendHash(chunkFile, compilationHash));
451
- }
452
-
453
- // Webpack outputs an array for each chunk when using sourcemaps
454
- // or when one chunk hosts js and css simultaneously
455
- const js = chunkFiles.find(chunkFile => /.js($|\?)/.test(chunkFile));
456
- if (js) {
457
- assets.chunks[chunkName].size = chunk.size;
458
- assets.chunks[chunkName].entry = js;
459
- assets.chunks[chunkName].hash = chunk.hash;
460
- assets.js.push(js);
461
- }
455
+ // Extract paths to .js and .css files from the current compilation
456
+ const extensionRegexp = /\.(css|js)(\?|$)/;
457
+ for (let i = 0; i < entryNames.length; i++) {
458
+ const entryName = entryNames[i];
459
+ const entryPointFiles = compilation.entrypoints.get(entryName).getFiles();
460
+ // Prepend the publicPath and append the hash depending on the
461
+ // webpack.output.publicPath and hashOptions
462
+ // E.g. bundle.js -> /bundle.js?hash
463
+ const entryPointPublicPaths = entryPointFiles
464
+ .map(chunkFile => {
465
+ const entryPointPublicPath = publicPath + chunkFile;
466
+ return this.options.hash
467
+ ? this.appendHash(entryPointPublicPath, compilationHash)
468
+ : entryPointPublicPath;
469
+ });
462
470
 
463
- // Gather all css files
464
- const css = chunkFiles.filter(chunkFile => /.css($|\?)/.test(chunkFile));
465
- assets.chunks[chunkName].css = css;
466
- assets.css = assets.css.concat(css);
471
+ entryPointPublicPaths.forEach((entryPointPublicPaths) => {
472
+ const extMatch = extensionRegexp.exec(entryPointPublicPaths);
473
+ // Skip if the public path is not a .css or .js file
474
+ if (!extMatch) {
475
+ return;
476
+ }
477
+ // ext will contain .js or .css
478
+ const ext = extMatch[1];
479
+ assets[ext].push({
480
+ entryName: entryName,
481
+ path: entryPointPublicPaths
482
+ });
483
+ });
467
484
  }
468
-
469
- // Duplicate css assets can occur on occasion if more than one chunk
470
- // requires the same css.
471
- assets.css = _.uniq(assets.css);
472
-
473
485
  return assets;
474
486
  }
475
487
 
476
488
  /**
477
489
  * Generate meta tags
490
+ * @returns {HtmlTagObject[]}
478
491
  */
479
492
  getMetaTags () {
480
- if (this.options.meta === false) {
493
+ const metaOptions = this.options.meta;
494
+ if (metaOptions === false) {
481
495
  return [];
482
496
  }
483
497
  // Make tags self-closing in case of xhtml
484
498
  // Turn { "viewport" : "width=500, initial-scale=1" } into
485
499
  // [{ name:"viewport" content:"width=500, initial-scale=1" }]
486
- const selfClosingTag = !!this.options.xhtml;
487
- const metaTagAttributeObjects = Object.keys(this.options.meta).map((metaName) => {
488
- const metaTagContent = this.options.meta[metaName];
489
- return (typeof metaTagContent === 'object') ? metaTagContent : {
500
+ const metaTagAttributeObjects = Object.keys(metaOptions).map((metaName) => {
501
+ const metaTagContent = metaOptions[metaName];
502
+ return (typeof metaTagContent === 'string') ? {
490
503
  name: metaName,
491
504
  content: metaTagContent
492
- };
505
+ } : metaTagContent;
493
506
  });
494
507
  // Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
495
508
  // the html-webpack-plugin tag structure
@@ -497,34 +510,41 @@ class HtmlWebpackPlugin {
497
510
  return {
498
511
  tagName: 'meta',
499
512
  voidTag: true,
500
- selfClosingTag: selfClosingTag,
501
513
  attributes: metaTagAttributes
502
514
  };
503
515
  });
504
516
  }
505
517
 
506
518
  /**
507
- * Injects the assets into the given html string
519
+ * Turns the given asset information into tag object representations
520
+ * which is seperated into head and body
521
+ *
522
+ * @param {{
523
+ js: {entryName: string, path: string}[],
524
+ css: {entryName: string, path: string}[],
525
+ favicon?: string
526
+ }} assets
527
+ *
528
+ * @returns {{
529
+ head: HtmlTagObject[],
530
+ body: HtmlTagObject[]
531
+ }}
508
532
  */
509
- generateHtmlTags (assets) {
533
+ generateHtmlTagObjects (assets) {
510
534
  // Turn script files into script tags
511
- const scripts = assets.js.map(scriptPath => ({
535
+ const scripts = assets.js.map(scriptAsset => ({
512
536
  tagName: 'script',
513
- closeTag: true,
537
+ voidTag: false,
514
538
  attributes: {
515
- type: 'text/javascript',
516
- src: scriptPath
539
+ src: scriptAsset.path
517
540
  }
518
541
  }));
519
- // Make tags self-closing in case of xhtml
520
- const selfClosingTag = !!this.options.xhtml;
521
542
  // Turn css files into link tags
522
- const styles = assets.css.map(stylePath => ({
543
+ const styles = assets.css.map(styleAsset => ({
523
544
  tagName: 'link',
524
- selfClosingTag: selfClosingTag,
525
545
  voidTag: true,
526
546
  attributes: {
527
- href: stylePath,
547
+ href: styleAsset.path,
528
548
  rel: 'stylesheet'
529
549
  }
530
550
  }));
@@ -536,7 +556,6 @@ class HtmlWebpackPlugin {
536
556
  if (assets.favicon) {
537
557
  head.push({
538
558
  tagName: 'link',
539
- selfClosingTag: selfClosingTag,
540
559
  voidTag: true,
541
560
  attributes: {
542
561
  rel: 'shortcut icon',
@@ -557,13 +576,24 @@ class HtmlWebpackPlugin {
557
576
 
558
577
  /**
559
578
  * Injects the assets into the given html string
579
+ *
580
+ * @param {string} html
581
+ * @param {any} assets
582
+ * The input html
583
+ * @param {{
584
+ head: HtmlTagObject[],
585
+ body: HtmlTagObject[]
586
+ }} assetTags
587
+ * The asset tags to inject
588
+ *
589
+ * @returns {string}
560
590
  */
561
591
  injectAssetsIntoHtml (html, assets, assetTags) {
562
592
  const htmlRegExp = /(<html[^>]*>)/i;
563
593
  const headRegExp = /(<\/head\s*>)/i;
564
594
  const bodyRegExp = /(<\/body\s*>)/i;
565
- const body = assetTags.body.map(this.createHtmlTag.bind(this));
566
- const head = assetTags.head.map(this.createHtmlTag.bind(this));
595
+ const body = assetTags.body.map((assetTagObject) => htmlTagObjectToString(assetTagObject, this.options.xhtml));
596
+ const head = assetTags.head.map((assetTagObject) => htmlTagObjectToString(assetTagObject, this.options.xhtml));
567
597
 
568
598
  if (body.length) {
569
599
  if (bodyRegExp.test(html)) {
@@ -603,7 +633,10 @@ class HtmlWebpackPlugin {
603
633
  }
604
634
 
605
635
  /**
606
- * Appends a cache busting hash
636
+ * Appends a cache busting hash to the query string of the url
637
+ * E.g. http://localhost:8080/ -> http://localhost:8080/?50c9096ba6183fd728eeb065a26ec175
638
+ * @param {string} url
639
+ * @param {string} hash
607
640
  */
608
641
  appendHash (url, hash) {
609
642
  if (!url) {
@@ -612,28 +645,12 @@ class HtmlWebpackPlugin {
612
645
  return url + (url.indexOf('?') === -1 ? '?' : '&') + hash;
613
646
  }
614
647
 
615
- /**
616
- * Turn a tag definition into a html string
617
- */
618
- createHtmlTag (tagDefinition) {
619
- const attributes = Object.keys(tagDefinition.attributes || {})
620
- .filter(attributeName => tagDefinition.attributes[attributeName] !== false)
621
- .map(attributeName => {
622
- if (tagDefinition.attributes[attributeName] === true) {
623
- return attributeName;
624
- }
625
- return attributeName + '="' + tagDefinition.attributes[attributeName] + '"';
626
- });
627
- // Backport of 3.x void tag definition
628
- const voidTag = tagDefinition.voidTag !== undefined ? tagDefinition.voidTag : !tagDefinition.closeTag;
629
- const selfClosingTag = tagDefinition.voidTag !== undefined ? tagDefinition.voidTag && this.options.xhtml : tagDefinition.selfClosingTag;
630
- return '<' + [tagDefinition.tagName].concat(attributes).join(' ') + (selfClosingTag ? '/' : '') + '>' +
631
- (tagDefinition.innerHTML || '') +
632
- (voidTag ? '' : '</' + tagDefinition.tagName + '>');
633
- }
634
-
635
648
  /**
636
649
  * Helper to return the absolute template path with a fallback loader
650
+ * @param {string} template
651
+ * The path to the tempalate e.g. './index.html'
652
+ * @param {string} context
653
+ * The webpack base resolution path for relative paths e.g. process.cwd()
637
654
  */
638
655
  getFullTemplatePath (template, context) {
639
656
  // If the template doesn't use a loader use the lodash template loader
@@ -655,59 +672,6 @@ class HtmlWebpackPlugin {
655
672
  files.sort();
656
673
  return files;
657
674
  }
658
-
659
- /**
660
- * Helper to promisify compilation.applyPluginsAsyncWaterfall that returns
661
- * a function that helps to merge given plugin arguments with processed ones
662
- */
663
- applyPluginsAsyncWaterfall (compilation) {
664
- if (compilation.hooks) {
665
- return (eventName, requiresResult, pluginArgs) => {
666
- const ccEventName = trainCaseToCamelCase(eventName);
667
- if (!compilation.hooks[ccEventName]) {
668
- compilation.errors.push(
669
- new Error('No hook found for ' + eventName)
670
- );
671
- }
672
-
673
- return compilation.hooks[ccEventName].promise(pluginArgs);
674
- };
675
- }
676
-
677
- // Before Webpack 4
678
- const promisedApplyPluginsAsyncWaterfall = function (name, init) {
679
- return new Promise((resolve, reject) => {
680
- const callback = function (err, result) {
681
- if (err) {
682
- return reject(err);
683
- }
684
- resolve(result);
685
- };
686
- compilation.applyPluginsAsyncWaterfall(name, init, callback);
687
- });
688
- };
689
-
690
- return (eventName, requiresResult, pluginArgs) => promisedApplyPluginsAsyncWaterfall(eventName, pluginArgs)
691
- .then(result => {
692
- if (requiresResult && !result) {
693
- compilation.warnings.push(
694
- new Error('Using ' + eventName + ' without returning a result is deprecated.')
695
- );
696
- }
697
- return _.extend(pluginArgs, result);
698
- });
699
- }
700
- }
701
-
702
- /**
703
- * Takes a string in train case and transforms it to camel case
704
- *
705
- * Example: 'hello-my-world' to 'helloMyWorld'
706
- *
707
- * @param {string} word
708
- */
709
- function trainCaseToCamelCase (word) {
710
- return word.replace(/-([\w])/g, (match, p1) => p1.toUpperCase());
711
675
  }
712
676
 
713
677
  /**
@@ -717,7 +681,6 @@ function trainCaseToCamelCase (word) {
717
681
  function templateParametersGenerator (compilation, assets, options) {
718
682
  return {
719
683
  compilation: compilation,
720
- webpack: compilation.getStats().toJson(),
721
684
  webpackConfig: compilation.options,
722
685
  htmlWebpackPlugin: {
723
686
  files: assets,
@@ -725,5 +688,4 @@ function templateParametersGenerator (compilation, assets, options) {
725
688
  }
726
689
  };
727
690
  }
728
-
729
691
  module.exports = HtmlWebpackPlugin;