html-webpack-plugin 4.0.0-alpha.1 → 4.0.0-beta.11

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,8 +1,9 @@
1
1
  // @ts-check
2
2
  // Import types
3
- /* eslint-disable */
4
- /// <reference path="./typings.d.ts" />
5
- /* eslint-enable */
3
+ /** @typedef {import("./typings").HtmlTagObject} HtmlTagObject */
4
+ /** @typedef {import("./typings").Options} HtmlWebpackOptions */
5
+ /** @typedef {import("./typings").ProcessedOptions} ProcessedHtmlWebpackOptions */
6
+ /** @typedef {import("./typings").TemplateParameter} TemplateParameter */
6
7
  /** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */
7
8
  /** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
8
9
  'use strict';
@@ -28,16 +29,16 @@ const fsReadFileAsync = promisify(fs.readFile);
28
29
 
29
30
  class HtmlWebpackPlugin {
30
31
  /**
31
- * @param {Partial<HtmlWebpackPluginOptions>} [options]
32
+ * @param {HtmlWebpackOptions} [options]
32
33
  */
33
34
  constructor (options) {
34
- /** @type {Partial<HtmlWebpackPluginOptions>} */
35
+ /** @type {HtmlWebpackOptions} */
35
36
  const userOptions = options || {};
36
37
 
37
38
  // Default options
38
- /** @type {HtmlWebpackPluginOptions} */
39
+ /** @type {ProcessedHtmlWebpackOptions} */
39
40
  const defaultOptions = {
40
- template: path.join(__dirname, 'default_index.ejs'),
41
+ template: 'auto',
41
42
  templateContent: false,
42
43
  templateParameters: templateParametersGenerator,
43
44
  filename: 'index.html',
@@ -45,18 +46,19 @@ class HtmlWebpackPlugin {
45
46
  inject: true,
46
47
  compile: true,
47
48
  favicon: false,
48
- minify: false,
49
+ minify: 'auto',
49
50
  cache: true,
50
51
  showErrors: true,
51
52
  chunks: 'all',
52
53
  excludeChunks: [],
53
54
  chunksSortMode: 'auto',
54
55
  meta: {},
56
+ base: false,
55
57
  title: 'Webpack App',
56
58
  xhtml: false
57
59
  };
58
60
 
59
- /** @type {HtmlWebpackPluginOptions} */
61
+ /** @type {ProcessedHtmlWebpackOptions} */
60
62
  this.options = Object.assign(defaultOptions, userOptions);
61
63
 
62
64
  // Default metaOptions if no template is provided
@@ -99,6 +101,31 @@ class HtmlWebpackPlugin {
99
101
  this.options.filename = path.relative(compiler.options.output.path, filename);
100
102
  }
101
103
 
104
+ // `contenthash` is introduced in webpack v4.3
105
+ // which conflicts with the plugin's existing `contenthash` method,
106
+ // hence it is renamed to `templatehash` to avoid conflicts
107
+ this.options.filename = this.options.filename.replace(/\[(?:(\w+):)?contenthash(?::([a-z]+\d*))?(?::(\d+))?\]/ig, (match) => {
108
+ return match.replace('contenthash', 'templatehash');
109
+ });
110
+
111
+ // Check if webpack is running in production mode
112
+ // @see https://github.com/webpack/webpack/blob/3366421f1784c449f415cda5930a8e445086f688/lib/WebpackOptionsDefaulter.js#L12-L14
113
+ const isProductionLikeMode = compiler.options.mode === 'production' || !compiler.options.mode;
114
+
115
+ const minify = this.options.minify;
116
+ if (minify === true || (minify === 'auto' && isProductionLikeMode)) {
117
+ /** @type { import('html-minifier').Options } */
118
+ this.options.minify = {
119
+ // https://github.com/kangax/html-minifier#options-quick-reference
120
+ collapseWhitespace: true,
121
+ removeComments: true,
122
+ removeRedundantAttributes: true,
123
+ removeScriptTypeAttributes: true,
124
+ removeStyleLinkTypeAttributes: true,
125
+ useShortDoctype: true
126
+ };
127
+ }
128
+
102
129
  // Clear the cache once a new HtmlWebpackPlugin is added
103
130
  childCompiler.clearCache(compiler);
104
131
 
@@ -192,11 +219,12 @@ class HtmlWebpackPlugin {
192
219
  // Turn the js and css paths into grouped HtmlTagObjects
193
220
  const assetTagGroupsPromise = assetsPromise
194
221
  // And allow third-party-plugin authors to reorder and change the assetTags before they are grouped
195
- .then(({assets}) => getHtmlWebpackPluginHooks(compilation).alterAssetTags.promise({
222
+ .then(({ assets }) => getHtmlWebpackPluginHooks(compilation).alterAssetTags.promise({
196
223
  assetTags: {
197
224
  scripts: self.generatedScriptTags(assets.js),
198
225
  styles: self.generateStyleTags(assets.css),
199
226
  meta: [
227
+ ...self.generateBaseTag(self.options.base),
200
228
  ...self.generatedMetaTags(self.options.meta),
201
229
  ...self.generateFaviconTags(assets.favicon)
202
230
  ]
@@ -204,7 +232,7 @@ class HtmlWebpackPlugin {
204
232
  outputName: childCompilationOutputName,
205
233
  plugin: self
206
234
  }))
207
- .then(({assetTags}) => {
235
+ .then(({ assetTags }) => {
208
236
  // Inject scripts to body unless it set explictly to head
209
237
  const scriptTarget = self.options.inject === 'head' ? 'head' : 'body';
210
238
  // Group assets to `head` and `body` tag arrays
@@ -237,51 +265,51 @@ class HtmlWebpackPlugin {
237
265
  : self.executeTemplate(compilationResult, assetsHookResult.assets, { headTags: assetTags.headTags, bodyTags: assetTags.bodyTags }, compilation));
238
266
 
239
267
  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
- });
268
+ // Allow plugins to change the html before assets are injected
269
+ .then(([assetTags, html]) => {
270
+ const pluginArgs = { html, headTags: assetTags.headTags, bodyTags: assetTags.bodyTags, plugin: self, outputName: childCompilationOutputName };
271
+ return getHtmlWebpackPluginHooks(compilation).afterTemplateExecution.promise(pluginArgs);
272
+ })
273
+ .then(({ html, headTags, bodyTags }) => {
274
+ return self.postProcessHtml(html, assets, { headTags, bodyTags });
275
+ });
248
276
 
249
277
  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));
270
- });
278
+ // Allow plugins to change the html after assets are injected
279
+ .then((html) => {
280
+ const pluginArgs = { html, plugin: self, outputName: childCompilationOutputName };
281
+ return getHtmlWebpackPluginHooks(compilation).beforeEmit.promise(pluginArgs)
282
+ .then(result => result.html);
283
+ })
284
+ .catch(err => {
285
+ // In case anything went wrong the promise is resolved
286
+ // with the error message and an error is logged
287
+ compilation.errors.push(prettyError(err, compiler.context).toString());
288
+ // Prevent caching
289
+ self.hash = null;
290
+ return self.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
291
+ })
292
+ .then(html => {
293
+ // Allow to use [templatehash] as placeholder for the html-webpack-plugin name
294
+ // See also https://survivejs.com/webpack/optimizing/adding-hashes-to-filenames/
295
+ // From https://github.com/webpack-contrib/extract-text-webpack-plugin/blob/8de6558e33487e7606e7cd7cb2adc2cccafef272/src/index.js#L212-L214
296
+ const finalOutputName = childCompilationOutputName.replace(/\[(?:(\w+):)?templatehash(?::([a-z]+\d*))?(?::(\d+))?\]/ig, (_, hashType, digestType, maxLength) => {
297
+ return loaderUtils.getHashDigest(Buffer.from(html, 'utf8'), hashType, digestType, parseInt(maxLength, 10));
298
+ });
271
299
  // 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));
300
+ compilation.assets[finalOutputName] = {
301
+ source: () => html,
302
+ size: () => html.length
303
+ };
304
+ return finalOutputName;
305
+ })
306
+ .then((finalOutputName) => getHtmlWebpackPluginHooks(compilation).afterEmit.promise({
307
+ outputName: finalOutputName,
308
+ plugin: self
309
+ }).catch(err => {
310
+ console.error(err);
311
+ return null;
312
+ }).then(() => null));
285
313
 
286
314
  // Once all files are added to the webpack compilation
287
315
  // let the webpack compiler continue
@@ -299,14 +327,14 @@ class HtmlWebpackPlugin {
299
327
  */
300
328
  evaluateCompilationResult (compilation, source) {
301
329
  if (!source) {
302
- return Promise.reject('The child compilation didn\'t provide a result');
330
+ return Promise.reject(new Error('The child compilation didn\'t provide a result'));
303
331
  }
304
332
  // The LibraryTemplatePlugin stores the template result in a local variable.
305
333
  // To extract the result during the evaluation this part has to be removed.
306
334
  source = source.replace('var HTML_WEBPACK_PLUGIN_RESULT =', '');
307
335
  const template = this.options.template.replace(/^.+!/, '').replace(/\?.+$/, '');
308
- const vmContext = vm.createContext(_.extend({HTML_WEBPACK_PLUGIN: true, require: require}, global));
309
- const vmScript = new vm.Script(source, {filename: template});
336
+ const vmContext = vm.createContext(_.extend({ HTML_WEBPACK_PLUGIN: true, require: require }, global));
337
+ const vmScript = new vm.Script(source, { filename: template });
310
338
  // Evaluate code and cast to string
311
339
  let newSource;
312
340
  try {
@@ -319,7 +347,7 @@ class HtmlWebpackPlugin {
319
347
  }
320
348
  return typeof newSource === 'string' || typeof newSource === 'function'
321
349
  ? Promise.resolve(newSource)
322
- : Promise.reject('The loader "' + this.options.template + '" didn\'t return html.');
350
+ : Promise.reject(new Error('The loader "' + this.options.template + '" didn\'t return html.'));
323
351
  }
324
352
 
325
353
  /**
@@ -327,8 +355,8 @@ class HtmlWebpackPlugin {
327
355
  * @param {WebpackCompilation} compilation
328
356
  * @param {{
329
357
  publicPath: string,
330
- js: Array<{entryName: string, path: string}>,
331
- css: Array<{entryName: string, path: string}>,
358
+ js: Array<string>,
359
+ css: Array<string>,
332
360
  manifest?: string,
333
361
  favicon?: string
334
362
  }} assets
@@ -336,17 +364,24 @@ class HtmlWebpackPlugin {
336
364
  headTags: HtmlTagObject[],
337
365
  bodyTags: HtmlTagObject[]
338
366
  }} assetTags
339
- * @returns {{[key: any]: any}}
367
+ * @returns {Promise<{[key: any]: any}>}
340
368
  */
341
369
  getTemplateParameters (compilation, assets, assetTags) {
342
- if (this.options.templateParameters === false) {
343
- return {};
370
+ const templateParameters = this.options.templateParameters;
371
+ if (templateParameters === false) {
372
+ return Promise.resolve({});
344
373
  }
345
- if (typeof this.options.templateParameters === 'function') {
346
- return this.options.templateParameters(compilation, assets, assetTags, this.options);
374
+ if (typeof templateParameters === 'function') {
375
+ const preparedAssetTags = {
376
+ headTags: this.prepareAssetTagGroupForRendering(assetTags.headTags),
377
+ bodyTags: this.prepareAssetTagGroupForRendering(assetTags.bodyTags)
378
+ };
379
+ return Promise
380
+ .resolve()
381
+ .then(() => templateParameters(compilation, assets, preparedAssetTags, this.options));
347
382
  }
348
- if (typeof this.options.templateParameters === 'object') {
349
- return this.options.templateParameters;
383
+ if (typeof templateParameters === 'object') {
384
+ return Promise.resolve(templateParameters);
350
385
  }
351
386
  throw new Error('templateParameters has to be either a function or an object');
352
387
  }
@@ -354,11 +389,11 @@ class HtmlWebpackPlugin {
354
389
  /**
355
390
  * This function renders the actual html by executing the template function
356
391
  *
357
- * @param {(templatePArameters) => string | Promise<string>} templateFunction
392
+ * @param {(templateParameters) => string | Promise<string>} templateFunction
358
393
  * @param {{
359
394
  publicPath: string,
360
- js: Array<{entryName: string, path: string}>,
361
- css: Array<{entryName: string, path: string}>,
395
+ js: Array<string>,
396
+ css: Array<string>,
362
397
  manifest?: string,
363
398
  favicon?: string
364
399
  }} assets
@@ -372,18 +407,17 @@ class HtmlWebpackPlugin {
372
407
  */
373
408
  executeTemplate (templateFunction, assets, assetTags, compilation) {
374
409
  // 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);
410
+ const templateParamsPromise = this.getTemplateParameters(compilation, assets, assetTags);
411
+ return templateParamsPromise.then((templateParams) => {
412
+ try {
413
+ // If html is a promise return the promise
414
+ // If html is a string turn it into a promise
415
+ return templateFunction(templateParams);
416
+ } catch (e) {
417
+ compilation.errors.push(new Error('Template execution failed: ' + e));
418
+ return Promise.reject(e);
419
+ }
420
+ });
387
421
  }
388
422
 
389
423
  /**
@@ -402,13 +436,13 @@ class HtmlWebpackPlugin {
402
436
  */
403
437
  postProcessHtml (html, assets, assetTags) {
404
438
  if (typeof html !== 'string') {
405
- return Promise.reject('Expected html to be a string but got ' + JSON.stringify(html));
439
+ return Promise.reject(new Error('Expected html to be a string but got ' + JSON.stringify(html)));
406
440
  }
407
441
  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)
442
+ ? this.injectAssetsIntoHtml(html, assets, assetTags)
443
+ : html;
444
+ const htmlAfterMinification = typeof this.options.minify === 'object'
445
+ ? require('html-minifier-terser').minify(htmlAfterInjection, this.options.minify)
412
446
  : htmlAfterInjection;
413
447
  return Promise.resolve(htmlAfterMinification);
414
448
  }
@@ -488,14 +522,14 @@ class HtmlWebpackPlugin {
488
522
  *
489
523
  * @param {{
490
524
  publicPath: string,
491
- js: Array<{entryName: string, path: string}>,
492
- css: Array<{entryName: string, path: string}>,
525
+ js: Array<string>,
526
+ css: Array<string>,
493
527
  manifest?: string,
494
528
  favicon?: string
495
529
  }} assets
496
530
  */
497
531
  isHotUpdateCompilation (assets) {
498
- return assets.js.length && assets.js.every(({entryName}) => /\.hot-update\.js$/.test(entryName));
532
+ return assets.js.length && assets.js.every((assetPath) => /\.hot-update\.js$/.test(assetPath));
499
533
  }
500
534
 
501
535
  /**
@@ -505,8 +539,8 @@ class HtmlWebpackPlugin {
505
539
  * @param {string[]} entryNames
506
540
  * @returns {{
507
541
  publicPath: string,
508
- js: Array<{entryName: string, path: string}>,
509
- css: Array<{entryName: string, path: string}>,
542
+ js: Array<string>,
543
+ css: Array<string>,
510
544
  manifest?: string,
511
545
  favicon?: string
512
546
  }}
@@ -516,12 +550,14 @@ class HtmlWebpackPlugin {
516
550
 
517
551
  /**
518
552
  * @type {string} the configured public path to the asset root
519
- * if a publicPath is set in the current webpack config use it otherwise
553
+ * if a path publicPath is set in the current webpack config use it otherwise
520
554
  * fallback to a realtive path
521
555
  */
522
- let publicPath = typeof compilation.options.output.publicPath !== 'undefined'
556
+ const webpackPublicPath = compilation.mainTemplate.getPublicPath({ hash: compilationHash });
557
+ const isPublicPathDefined = webpackPublicPath.trim() !== '';
558
+ let publicPath = isPublicPathDefined
523
559
  // If a hard coded public path exists use it
524
- ? compilation.mainTemplate.getPublicPath({hash: compilationHash})
560
+ ? webpackPublicPath
525
561
  // If no public path was set get a relative url path
526
562
  : path.relative(path.resolve(compilation.options.output.path, path.dirname(childCompilationOutputName)), compilation.options.output.path)
527
563
  .split(path.sep).join('/');
@@ -533,8 +569,8 @@ class HtmlWebpackPlugin {
533
569
  /**
534
570
  * @type {{
535
571
  publicPath: string,
536
- js: Array<{entryName: string, path: string}>,
537
- css: Array<{entryName: string, path: string}>,
572
+ js: Array<string>,
573
+ css: Array<string>,
538
574
  manifest?: string,
539
575
  favicon?: string
540
576
  }}
@@ -542,7 +578,7 @@ class HtmlWebpackPlugin {
542
578
  const assets = {
543
579
  // The public path
544
580
  publicPath: publicPath,
545
- // Will contain all js files
581
+ // Will contain all js and mjs files
546
582
  js: [],
547
583
  // Will contain all css files
548
584
  css: [],
@@ -557,8 +593,9 @@ class HtmlWebpackPlugin {
557
593
  assets.manifest = this.appendHash(assets.manifest, compilationHash);
558
594
  }
559
595
 
560
- // Extract paths to .js and .css files from the current compilation
561
- const extensionRegexp = /\.(css|js)(\?|$)/;
596
+ // Extract paths to .js, .mjs and .css files from the current compilation
597
+ const entryPointPublicPathMap = {};
598
+ const extensionRegexp = /\.(css|js|mjs)(\?|$)/;
562
599
  for (let i = 0; i < entryNames.length; i++) {
563
600
  const entryName = entryNames[i];
564
601
  const entryPointFiles = compilation.entrypoints.get(entryName).getFiles();
@@ -567,24 +604,27 @@ class HtmlWebpackPlugin {
567
604
  // E.g. bundle.js -> /bundle.js?hash
568
605
  const entryPointPublicPaths = entryPointFiles
569
606
  .map(chunkFile => {
570
- const entryPointPublicPath = publicPath + chunkFile;
607
+ const entryPointPublicPath = publicPath + this.urlencodePath(chunkFile);
571
608
  return this.options.hash
572
609
  ? this.appendHash(entryPointPublicPath, compilationHash)
573
610
  : entryPointPublicPath;
574
611
  });
575
612
 
576
- entryPointPublicPaths.forEach((entryPointPublicPaths) => {
577
- const extMatch = extensionRegexp.exec(entryPointPublicPaths);
578
- // Skip if the public path is not a .css or .js file
613
+ entryPointPublicPaths.forEach((entryPointPublicPath) => {
614
+ const extMatch = extensionRegexp.exec(entryPointPublicPath);
615
+ // Skip if the public path is not a .css, .mjs or .js file
579
616
  if (!extMatch) {
580
617
  return;
581
618
  }
582
- // ext will contain .js or .css
583
- const ext = extMatch[1];
584
- assets[ext].push({
585
- entryName: entryName,
586
- path: entryPointPublicPaths
587
- });
619
+ // Skip if this file is already known
620
+ // (e.g. because of common chunk optimizations)
621
+ if (entryPointPublicPathMap[entryPointPublicPath]) {
622
+ return;
623
+ }
624
+ entryPointPublicPathMap[entryPointPublicPath] = true;
625
+ // ext will contain .js or .css, because .mjs recognizes as .js
626
+ const ext = extMatch[1] === 'mjs' ? 'js' : extMatch[1];
627
+ assets[ext].push(entryPointPublicPath);
588
628
  });
589
629
  }
590
630
  return assets;
@@ -596,7 +636,7 @@ class HtmlWebpackPlugin {
596
636
  *
597
637
  * @param {string|false} faviconFilePath
598
638
  * @param {WebpackCompilation} compilation
599
- * @parma {string} publicPath
639
+ * @param {string} publicPath
600
640
  * @returns {Promise<string|undefined>}
601
641
  */
602
642
  getFaviconPublicPath (faviconFilePath, compilation, publicPath) {
@@ -626,14 +666,14 @@ class HtmlWebpackPlugin {
626
666
  // Turn { "viewport" : "width=500, initial-scale=1" } into
627
667
  // [{ name:"viewport" content:"width=500, initial-scale=1" }]
628
668
  const metaTagAttributeObjects = Object.keys(metaOptions)
629
- .map((metaName) => {
630
- const metaTagContent = metaOptions[metaName];
631
- return (typeof metaTagContent === 'string') ? {
632
- name: metaName,
633
- content: metaTagContent
634
- } : metaTagContent;
635
- })
636
- .filter((attribute) => attribute !== false);
669
+ .map((metaName) => {
670
+ const metaTagContent = metaOptions[metaName];
671
+ return (typeof metaTagContent === 'string') ? {
672
+ name: metaName,
673
+ content: metaTagContent
674
+ } : metaTagContent;
675
+ })
676
+ .filter((attribute) => attribute !== false);
637
677
  // Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
638
678
  // the html-webpack-plugin tag structure
639
679
  return metaTagAttributeObjects.map((metaTagAttributes) => {
@@ -650,42 +690,64 @@ class HtmlWebpackPlugin {
650
690
 
651
691
  /**
652
692
  * Generate all tags script for the given file paths
653
- * @param {Array<{ entryName: string; path: string; }>} jsAssets
693
+ * @param {Array<string>} jsAssets
654
694
  * @returns {Array<HtmlTagObject>}
655
695
  */
656
696
  generatedScriptTags (jsAssets) {
657
697
  return jsAssets.map(scriptAsset => ({
658
698
  tagName: 'script',
659
699
  voidTag: false,
660
- entry: scriptAsset.entryName,
661
700
  attributes: {
662
- src: scriptAsset.path
701
+ src: scriptAsset
663
702
  }
664
703
  }));
665
704
  }
666
705
 
667
706
  /**
668
707
  * Generate all style tags for the given file paths
669
- * @param {Array<{ entryName: string; path: string; }>} cssAssets
708
+ * @param {Array<string>} cssAssets
670
709
  * @returns {Array<HtmlTagObject>}
671
710
  */
672
711
  generateStyleTags (cssAssets) {
673
712
  return cssAssets.map(styleAsset => ({
674
713
  tagName: 'link',
675
714
  voidTag: true,
676
- entry: styleAsset.entryName,
677
715
  attributes: {
678
- href: styleAsset.path,
716
+ href: styleAsset,
679
717
  rel: 'stylesheet'
680
718
  }
681
719
  }));
682
720
  }
683
721
 
722
+ /**
723
+ * Generate an optional base tag
724
+ * @param { false
725
+ | string
726
+ | {[attributeName: string]: string} // attributes e.g. { href:"http://example.com/page.html" target:"_blank" }
727
+ } baseOption
728
+ * @returns {Array<HtmlTagObject>}
729
+ */
730
+ generateBaseTag (baseOption) {
731
+ if (baseOption === false) {
732
+ return [];
733
+ } else {
734
+ return [{
735
+ tagName: 'base',
736
+ voidTag: true,
737
+ attributes: (typeof baseOption === 'string') ? {
738
+ href: baseOption
739
+ } : baseOption
740
+ }];
741
+ }
742
+ }
743
+
684
744
  /**
685
745
  * Generate all meta tags for the given meta configuration
686
746
  * @param {false | {
687
- [name: string]: string|false // name content pair e.g. {viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no'}`
688
- | {[attributeName: string]: string|boolean} // custom properties e.g. { name:"viewport" content:"width=500, initial-scale=1" }
747
+ [name: string]:
748
+ false // disabled
749
+ | string // name content pair e.g. {viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no'}`
750
+ | {[attributeName: string]: string|boolean} // custom properties e.g. { name:"viewport" content:"width=500, initial-scale=1" }
689
751
  }} metaOptions
690
752
  * @returns {Array<HtmlTagObject>}
691
753
  */
@@ -693,9 +755,9 @@ class HtmlWebpackPlugin {
693
755
  if (metaOptions === false) {
694
756
  return [];
695
757
  }
696
- // Make tags self-closing in case of xhtml
697
- // Turn { "viewport" : "width=500, initial-scale=1" } into
698
- // [{ name:"viewport" content:"width=500, initial-scale=1" }]
758
+ // Make tags self-closing in case of xhtml
759
+ // Turn { "viewport" : "width=500, initial-scale=1" } into
760
+ // [{ name:"viewport" content:"width=500, initial-scale=1" }]
699
761
  const metaTagAttributeObjects = Object.keys(metaOptions)
700
762
  .map((metaName) => {
701
763
  const metaTagContent = metaOptions[metaName];
@@ -771,6 +833,28 @@ class HtmlWebpackPlugin {
771
833
  return result;
772
834
  }
773
835
 
836
+ /**
837
+ * Add toString methods for easier rendering
838
+ * inside the template
839
+ *
840
+ * @param {Array<HtmlTagObject>} assetTagGroup
841
+ * @returns {Array<HtmlTagObject>}
842
+ */
843
+ prepareAssetTagGroupForRendering (assetTagGroup) {
844
+ const xhtml = this.options.xhtml;
845
+ const preparedTags = assetTagGroup.map((assetTag) => {
846
+ const copiedAssetTag = Object.assign({}, assetTag);
847
+ copiedAssetTag.toString = function () {
848
+ return htmlTagObjectToString(this, xhtml);
849
+ };
850
+ return copiedAssetTag;
851
+ });
852
+ preparedTags.toString = function () {
853
+ return this.join('');
854
+ };
855
+ return preparedTags;
856
+ }
857
+
774
858
  /**
775
859
  * Injects the assets into the given html string
776
860
  *
@@ -842,14 +926,30 @@ class HtmlWebpackPlugin {
842
926
  return url + (url.indexOf('?') === -1 ? '?' : '&') + hash;
843
927
  }
844
928
 
929
+ /**
930
+ * Encode each path component using `encodeURIComponent` as files can contain characters
931
+ * which needs special encoding in URLs like `+ `.
932
+ *
933
+ * @param {string} filePath
934
+ */
935
+ urlencodePath (filePath) {
936
+ return filePath.split('/').map(encodeURIComponent).join('/');
937
+ }
938
+
845
939
  /**
846
940
  * Helper to return the absolute template path with a fallback loader
847
941
  * @param {string} template
848
- * The path to the tempalate e.g. './index.html'
942
+ * The path to the template e.g. './index.html'
849
943
  * @param {string} context
850
944
  * The webpack base resolution path for relative paths e.g. process.cwd()
851
945
  */
852
946
  getFullTemplatePath (template, context) {
947
+ if (template === 'auto') {
948
+ template = path.resolve(context, 'src/index.ejs');
949
+ if (!fs.existsSync(template)) {
950
+ template = path.join(__dirname, 'default_index.ejs');
951
+ }
952
+ }
853
953
  // If the template doesn't use a loader use the lodash template loader
854
954
  if (template.indexOf('!') === -1) {
855
955
  template = require.resolve('./lib/loader.js') + '!' + path.resolve(context, template);
@@ -879,8 +979,8 @@ class HtmlWebpackPlugin {
879
979
  * @param {WebpackCompilation} compilation
880
980
  * @param {{
881
981
  publicPath: string,
882
- js: Array<{entryName: string, path: string}>,
883
- css: Array<{entryName: string, path: string}>,
982
+ js: Array<string>,
983
+ css: Array<string>,
884
984
  manifest?: string,
885
985
  favicon?: string
886
986
  }} assets
@@ -888,8 +988,8 @@ class HtmlWebpackPlugin {
888
988
  headTags: HtmlTagObject[],
889
989
  bodyTags: HtmlTagObject[]
890
990
  }} assetTags
891
- * @param {HtmlWebpackPluginOptions} options
892
- * @returns {HtmlWebpackPluginTemplateParameter}
991
+ * @param {ProcessedHtmlWebpackOptions} options
992
+ * @returns {TemplateParameter}
893
993
  */
894
994
  function templateParametersGenerator (compilation, assets, assetTags, options) {
895
995
  return {
@@ -912,7 +1012,7 @@ HtmlWebpackPlugin.version = 4;
912
1012
  /**
913
1013
  * A static helper to get the hooks for this plugin
914
1014
  *
915
- * Usage: HtmlWebpackPlugin.getHook(compilation, 'HookName').tap('YourPluginName', () => { ... });
1015
+ * Usage: HtmlWebpackPlugin.getHooks(compilation).HOOK_NAME.tapAsync('YourPluginName', () => { ... });
916
1016
  */
917
1017
  HtmlWebpackPlugin.getHooks = getHtmlWebpackPluginHooks;
918
1018
  HtmlWebpackPlugin.createHtmlTagObject = createHtmlTagObject;