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