index.js 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210
  1. // @ts-check
  2. 'use strict';
  3. const promisify = require('util').promisify;
  4. const vm = require('vm');
  5. const fs = require('fs');
  6. const _ = require('lodash');
  7. const path = require('path');
  8. const { CachedChildCompilation } = require('./lib/cached-child-compiler');
  9. const { createHtmlTagObject, htmlTagObjectToString, HtmlTagArray } = require('./lib/html-tags');
  10. const prettyError = require('./lib/errors.js');
  11. const chunkSorter = require('./lib/chunksorter.js');
  12. const getHtmlWebpackPluginHooks = require('./lib/hooks.js').getHtmlWebpackPluginHooks;
  13. /** @typedef {import("./typings").HtmlTagObject} HtmlTagObject */
  14. /** @typedef {import("./typings").Options} HtmlWebpackOptions */
  15. /** @typedef {import("./typings").ProcessedOptions} ProcessedHtmlWebpackOptions */
  16. /** @typedef {import("./typings").TemplateParameter} TemplateParameter */
  17. /** @typedef {import("webpack").Compiler} Compiler */
  18. /** @typedef {ReturnType<Compiler["getInfrastructureLogger"]>} Logger */
  19. /** @typedef {import("webpack/lib/Compilation.js")} Compilation */
  20. /** @typedef {Array<{ name: string, source: import('webpack').sources.Source, info?: import('webpack').AssetInfo }>} PreviousEmittedAssets */
  21. /** @typedef {{ publicPath: string, js: Array<string>, css: Array<string>, manifest?: string, favicon?: string }} AssetsInformationByGroups */
  22. class HtmlWebpackPlugin {
  23. /**
  24. * @param {HtmlWebpackOptions} [options]
  25. */
  26. constructor (options) {
  27. /** @type {HtmlWebpackOptions} */
  28. // TODO remove me in the next major release
  29. this.userOptions = options || {};
  30. this.version = HtmlWebpackPlugin.version;
  31. // Default options
  32. /** @type {ProcessedHtmlWebpackOptions} */
  33. const defaultOptions = {
  34. template: 'auto',
  35. templateContent: false,
  36. templateParameters: templateParametersGenerator,
  37. filename: 'index.html',
  38. publicPath: this.userOptions.publicPath === undefined ? 'auto' : this.userOptions.publicPath,
  39. hash: false,
  40. inject: this.userOptions.scriptLoading === 'blocking' ? 'body' : 'head',
  41. scriptLoading: 'defer',
  42. compile: true,
  43. favicon: false,
  44. minify: 'auto',
  45. cache: true,
  46. showErrors: true,
  47. chunks: 'all',
  48. excludeChunks: [],
  49. chunksSortMode: 'auto',
  50. meta: {},
  51. base: false,
  52. title: 'Webpack App',
  53. xhtml: false
  54. };
  55. /** @type {ProcessedHtmlWebpackOptions} */
  56. this.options = Object.assign(defaultOptions, this.userOptions);
  57. }
  58. /**
  59. *
  60. * @param {Compiler} compiler
  61. * @returns {void}
  62. */
  63. apply (compiler) {
  64. this.logger = compiler.getInfrastructureLogger('HtmlWebpackPlugin');
  65. // Wait for configuration preset plugions to apply all configure webpack defaults
  66. compiler.hooks.initialize.tap('HtmlWebpackPlugin', () => {
  67. const options = this.options;
  68. options.template = this.getTemplatePath(this.options.template, compiler.context);
  69. // Assert correct option spelling
  70. if (options.scriptLoading !== 'defer' && options.scriptLoading !== 'blocking' && options.scriptLoading !== 'module' && options.scriptLoading !== 'systemjs-module') {
  71. /** @type {Logger} */
  72. (this.logger).error('The "scriptLoading" option need to be set to "defer", "blocking" or "module" or "systemjs-module"');
  73. }
  74. if (options.inject !== true && options.inject !== false && options.inject !== 'head' && options.inject !== 'body') {
  75. /** @type {Logger} */
  76. (this.logger).error('The `inject` option needs to be set to true, false, "head" or "body');
  77. }
  78. if (
  79. this.options.templateParameters !== false &&
  80. typeof this.options.templateParameters !== 'function' &&
  81. typeof this.options.templateParameters !== 'object'
  82. ) {
  83. /** @type {Logger} */
  84. (this.logger).error('The `templateParameters` has to be either a function or an object or false');
  85. }
  86. // Default metaOptions if no template is provided
  87. if (!this.userOptions.template && options.templateContent === false && options.meta) {
  88. options.meta = Object.assign(
  89. {},
  90. options.meta,
  91. {
  92. // TODO remove in the next major release
  93. // From https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag
  94. viewport: 'width=device-width, initial-scale=1'
  95. },
  96. this.userOptions.meta
  97. );
  98. }
  99. // entryName to fileName conversion function
  100. const userOptionFilename = this.userOptions.filename || this.options.filename;
  101. const filenameFunction = typeof userOptionFilename === 'function'
  102. ? userOptionFilename
  103. // Replace '[name]' with entry name
  104. : (entryName) => userOptionFilename.replace(/\[name\]/g, entryName);
  105. /** output filenames for the given entry names */
  106. const entryNames = Object.keys(compiler.options.entry);
  107. const outputFileNames = new Set((entryNames.length ? entryNames : ['main']).map(filenameFunction));
  108. // Hook all options into the webpack compiler
  109. outputFileNames.forEach((outputFileName) => {
  110. // Instance variables to keep caching information for multiple builds
  111. const assetJson = { value: undefined };
  112. /**
  113. * store the previous generated asset to emit them even if the content did not change
  114. * to support watch mode for third party plugins like the clean-webpack-plugin or the compression plugin
  115. * @type {PreviousEmittedAssets}
  116. */
  117. const previousEmittedAssets = [];
  118. // Inject child compiler plugin
  119. const childCompilerPlugin = new CachedChildCompilation(compiler);
  120. if (!this.options.templateContent) {
  121. childCompilerPlugin.addEntry(this.options.template);
  122. }
  123. // convert absolute filename into relative so that webpack can
  124. // generate it at correct location
  125. let filename = outputFileName;
  126. if (path.resolve(filename) === path.normalize(filename)) {
  127. const outputPath = /** @type {string} - Once initialized the path is always a string */(compiler.options.output.path);
  128. filename = path.relative(outputPath, filename);
  129. }
  130. compiler.hooks.thisCompilation.tap('HtmlWebpackPlugin',
  131. /**
  132. * Hook into the webpack compilation
  133. * @param {Compilation} compilation
  134. */
  135. (compilation) => {
  136. compilation.hooks.processAssets.tapAsync(
  137. {
  138. name: 'HtmlWebpackPlugin',
  139. stage:
  140. /**
  141. * Generate the html after minification and dev tooling is done
  142. */
  143. compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE
  144. },
  145. /**
  146. * Hook into the process assets hook
  147. * @param {any} _
  148. * @param {(err?: Error) => void} callback
  149. */
  150. (_, callback) => {
  151. this.generateHTML(compiler, compilation, filename, childCompilerPlugin, previousEmittedAssets, assetJson, callback);
  152. });
  153. });
  154. });
  155. });
  156. }
  157. /**
  158. * Helper to return the absolute template path with a fallback loader
  159. *
  160. * @private
  161. * @param {string} template The path to the template e.g. './index.html'
  162. * @param {string} context The webpack base resolution path for relative paths e.g. process.cwd()
  163. */
  164. getTemplatePath (template, context) {
  165. if (template === 'auto') {
  166. template = path.resolve(context, 'src/index.ejs');
  167. if (!fs.existsSync(template)) {
  168. template = path.join(__dirname, 'default_index.ejs');
  169. }
  170. }
  171. // If the template doesn't use a loader use the lodash template loader
  172. if (template.indexOf('!') === -1) {
  173. template = require.resolve('./lib/loader.js') + '!' + path.resolve(context, template);
  174. }
  175. // Resolve template path
  176. return template.replace(
  177. /([!])([^/\\][^!?]+|[^/\\!?])($|\?[^!?\n]+$)/,
  178. (match, prefix, filepath, postfix) => prefix + path.resolve(filepath) + postfix);
  179. }
  180. /**
  181. * Return all chunks from the compilation result which match the exclude and include filters
  182. *
  183. * @private
  184. * @param {any} chunks
  185. * @param {string[]|'all'} includedChunks
  186. * @param {string[]} excludedChunks
  187. */
  188. filterEntryChunks (chunks, includedChunks, excludedChunks) {
  189. return chunks.filter(chunkName => {
  190. // Skip if the chunks should be filtered and the given chunk was not added explicity
  191. if (Array.isArray(includedChunks) && includedChunks.indexOf(chunkName) === -1) {
  192. return false;
  193. }
  194. // Skip if the chunks should be filtered and the given chunk was excluded explicity
  195. if (Array.isArray(excludedChunks) && excludedChunks.indexOf(chunkName) !== -1) {
  196. return false;
  197. }
  198. // Add otherwise
  199. return true;
  200. });
  201. }
  202. /**
  203. * Helper to sort chunks
  204. *
  205. * @private
  206. * @param {string[]} entryNames
  207. * @param {string|((entryNameA: string, entryNameB: string) => number)} sortMode
  208. * @param {Compilation} compilation
  209. */
  210. sortEntryChunks (entryNames, sortMode, compilation) {
  211. // Custom function
  212. if (typeof sortMode === 'function') {
  213. return entryNames.sort(sortMode);
  214. }
  215. // Check if the given sort mode is a valid chunkSorter sort mode
  216. if (typeof chunkSorter[sortMode] !== 'undefined') {
  217. return chunkSorter[sortMode](entryNames, compilation, this.options);
  218. }
  219. throw new Error('"' + sortMode + '" is not a valid chunk sort mode');
  220. }
  221. /**
  222. * Encode each path component using `encodeURIComponent` as files can contain characters
  223. * which needs special encoding in URLs like `+ `.
  224. *
  225. * Valid filesystem characters which need to be encoded for urls:
  226. *
  227. * # pound, % percent, & ampersand, { left curly bracket, } right curly bracket,
  228. * \ back slash, < left angle bracket, > right angle bracket, * asterisk, ? question mark,
  229. * blank spaces, $ dollar sign, ! exclamation point, ' single quotes, " double quotes,
  230. * : colon, @ at sign, + plus sign, ` backtick, | pipe, = equal sign
  231. *
  232. * However the query string must not be encoded:
  233. *
  234. * fo:demonstration-path/very fancy+name.js?path=/home?value=abc&value=def#zzz
  235. * ^ ^ ^ ^ ^ ^ ^ ^^ ^ ^ ^ ^ ^
  236. * | | | | | | | || | | | | |
  237. * encoded | | encoded | | || | | | | |
  238. * ignored ignored ignored ignored ignored
  239. *
  240. * @private
  241. * @param {string} filePath
  242. */
  243. urlencodePath (filePath) {
  244. // People use the filepath in quite unexpected ways.
  245. // Try to extract the first querystring of the url:
  246. //
  247. // some+path/demo.html?value=abc?def
  248. //
  249. const queryStringStart = filePath.indexOf('?');
  250. const urlPath = queryStringStart === -1 ? filePath : filePath.substr(0, queryStringStart);
  251. const queryString = filePath.substr(urlPath.length);
  252. // Encode all parts except '/' which are not part of the querystring:
  253. const encodedUrlPath = urlPath.split('/').map(encodeURIComponent).join('/');
  254. return encodedUrlPath + queryString;
  255. }
  256. /**
  257. * Appends a cache busting hash to the query string of the url
  258. * E.g. http://localhost:8080/ -> http://localhost:8080/?50c9096ba6183fd728eeb065a26ec175
  259. *
  260. * @private
  261. * @param {string} url
  262. * @param {string} hash
  263. */
  264. appendHash (url, hash) {
  265. if (!url) {
  266. return url;
  267. }
  268. return url + (url.indexOf('?') === -1 ? '?' : '&') + hash;
  269. }
  270. /**
  271. * Generate the relative or absolute base url to reference images, css, and javascript files
  272. * from within the html file - the publicPath
  273. *
  274. * @private
  275. * @param {Compilation} compilation
  276. * @param {string} filename
  277. * @param {string | 'auto'} customPublicPath
  278. * @returns {string}
  279. */
  280. getPublicPath (compilation, filename, customPublicPath) {
  281. /**
  282. * @type {string} the configured public path to the asset root
  283. * if a path publicPath is set in the current webpack config use it otherwise
  284. * fallback to a relative path
  285. */
  286. const webpackPublicPath = compilation.getAssetPath(compilation.outputOptions.publicPath, { hash: compilation.hash });
  287. // Webpack 5 introduced "auto" as default value
  288. const isPublicPathDefined = webpackPublicPath !== 'auto';
  289. let publicPath =
  290. // If the html-webpack-plugin options contain a custom public path uset it
  291. customPublicPath !== 'auto'
  292. ? customPublicPath
  293. : (isPublicPathDefined
  294. // If a hard coded public path exists use it
  295. ? webpackPublicPath
  296. // If no public path was set get a relative url path
  297. : path.relative(path.resolve(compilation.options.output.path, path.dirname(filename)), compilation.options.output.path)
  298. .split(path.sep).join('/')
  299. );
  300. if (publicPath.length && publicPath.substr(-1, 1) !== '/') {
  301. publicPath += '/';
  302. }
  303. return publicPath;
  304. }
  305. /**
  306. * The getAssetsForHTML extracts the asset information of a webpack compilation for all given entry names.
  307. *
  308. * @private
  309. * @param {Compilation} compilation
  310. * @param {string} outputName
  311. * @param {string[]} entryNames
  312. * @returns {AssetsInformationByGroups}
  313. */
  314. getAssetsInformationByGroups (compilation, outputName, entryNames) {
  315. /** The public path used inside the html file */
  316. const publicPath = this.getPublicPath(compilation, outputName, this.options.publicPath);
  317. /**
  318. * @type {AssetsInformationByGroups}
  319. */
  320. const assets = {
  321. // The public path
  322. publicPath,
  323. // Will contain all js and mjs files
  324. js: [],
  325. // Will contain all css files
  326. css: [],
  327. // Will contain the html5 appcache manifest files if it exists
  328. manifest: Object.keys(compilation.assets).find(assetFile => path.extname(assetFile) === '.appcache'),
  329. // Favicon
  330. favicon: undefined
  331. };
  332. // Append a hash for cache busting
  333. if (this.options.hash && assets.manifest) {
  334. assets.manifest = this.appendHash(assets.manifest, /** @type {string} */ (compilation.hash));
  335. }
  336. // Extract paths to .js, .mjs and .css files from the current compilation
  337. const entryPointPublicPathMap = {};
  338. const extensionRegexp = /\.(css|js|mjs)(\?|$)/;
  339. for (let i = 0; i < entryNames.length; i++) {
  340. const entryName = entryNames[i];
  341. /** entryPointUnfilteredFiles - also includes hot module update files */
  342. const entryPointUnfilteredFiles = compilation.entrypoints.get(entryName).getFiles();
  343. const entryPointFiles = entryPointUnfilteredFiles.filter((chunkFile) => {
  344. const asset = compilation.getAsset(chunkFile);
  345. if (!asset) {
  346. return true;
  347. }
  348. // Prevent hot-module files from being included:
  349. const assetMetaInformation = asset.info || {};
  350. return !(assetMetaInformation.hotModuleReplacement || assetMetaInformation.development);
  351. });
  352. // Prepend the publicPath and append the hash depending on the
  353. // webpack.output.publicPath and hashOptions
  354. // E.g. bundle.js -> /bundle.js?hash
  355. const entryPointPublicPaths = entryPointFiles
  356. .map(chunkFile => {
  357. const entryPointPublicPath = publicPath + this.urlencodePath(chunkFile);
  358. return this.options.hash
  359. ? this.appendHash(entryPointPublicPath, compilation.hash)
  360. : entryPointPublicPath;
  361. });
  362. entryPointPublicPaths.forEach((entryPointPublicPath) => {
  363. const extMatch = extensionRegexp.exec(entryPointPublicPath);
  364. // Skip if the public path is not a .css, .mjs or .js file
  365. if (!extMatch) {
  366. return;
  367. }
  368. // Skip if this file is already known
  369. // (e.g. because of common chunk optimizations)
  370. if (entryPointPublicPathMap[entryPointPublicPath]) {
  371. return;
  372. }
  373. entryPointPublicPathMap[entryPointPublicPath] = true;
  374. // ext will contain .js or .css, because .mjs recognizes as .js
  375. const ext = extMatch[1] === 'mjs' ? 'js' : extMatch[1];
  376. assets[ext].push(entryPointPublicPath);
  377. });
  378. }
  379. return assets;
  380. }
  381. /**
  382. * Once webpack is done with compiling the template into a NodeJS code this function
  383. * evaluates it to generate the html result
  384. *
  385. * The evaluateCompilationResult is only a class function to allow spying during testing.
  386. * Please change that in a further refactoring
  387. *
  388. * @param {string} source
  389. * @param {string} publicPath
  390. * @param {string} templateFilename
  391. * @returns {Promise<string | (() => string | Promise<string>)>}
  392. */
  393. evaluateCompilationResult (source, publicPath, templateFilename) {
  394. if (!source) {
  395. return Promise.reject(new Error('The child compilation didn\'t provide a result'));
  396. }
  397. // The LibraryTemplatePlugin stores the template result in a local variable.
  398. // By adding it to the end the value gets extracted during evaluation
  399. if (source.indexOf('HTML_WEBPACK_PLUGIN_RESULT') >= 0) {
  400. source += ';\nHTML_WEBPACK_PLUGIN_RESULT';
  401. }
  402. const templateWithoutLoaders = templateFilename.replace(/^.+!/, '').replace(/\?.+$/, '');
  403. const vmContext = vm.createContext({
  404. ...global,
  405. HTML_WEBPACK_PLUGIN: true,
  406. require: require,
  407. htmlWebpackPluginPublicPath: publicPath,
  408. __filename: templateWithoutLoaders,
  409. __dirname: path.dirname(templateWithoutLoaders),
  410. AbortController: global.AbortController,
  411. AbortSignal: global.AbortSignal,
  412. Blob: global.Blob,
  413. Buffer: global.Buffer,
  414. ByteLengthQueuingStrategy: global.ByteLengthQueuingStrategy,
  415. BroadcastChannel: global.BroadcastChannel,
  416. CompressionStream: global.CompressionStream,
  417. CountQueuingStrategy: global.CountQueuingStrategy,
  418. Crypto: global.Crypto,
  419. CryptoKey: global.CryptoKey,
  420. CustomEvent: global.CustomEvent,
  421. DecompressionStream: global.DecompressionStream,
  422. Event: global.Event,
  423. EventTarget: global.EventTarget,
  424. File: global.File,
  425. FormData: global.FormData,
  426. Headers: global.Headers,
  427. MessageChannel: global.MessageChannel,
  428. MessageEvent: global.MessageEvent,
  429. MessagePort: global.MessagePort,
  430. PerformanceEntry: global.PerformanceEntry,
  431. PerformanceMark: global.PerformanceMark,
  432. PerformanceMeasure: global.PerformanceMeasure,
  433. PerformanceObserver: global.PerformanceObserver,
  434. PerformanceObserverEntryList: global.PerformanceObserverEntryList,
  435. PerformanceResourceTiming: global.PerformanceResourceTiming,
  436. ReadableByteStreamController: global.ReadableByteStreamController,
  437. ReadableStream: global.ReadableStream,
  438. ReadableStreamBYOBReader: global.ReadableStreamBYOBReader,
  439. ReadableStreamBYOBRequest: global.ReadableStreamBYOBRequest,
  440. ReadableStreamDefaultController: global.ReadableStreamDefaultController,
  441. ReadableStreamDefaultReader: global.ReadableStreamDefaultReader,
  442. Response: global.Response,
  443. Request: global.Request,
  444. SubtleCrypto: global.SubtleCrypto,
  445. DOMException: global.DOMException,
  446. TextDecoder: global.TextDecoder,
  447. TextDecoderStream: global.TextDecoderStream,
  448. TextEncoder: global.TextEncoder,
  449. TextEncoderStream: global.TextEncoderStream,
  450. TransformStream: global.TransformStream,
  451. TransformStreamDefaultController: global.TransformStreamDefaultController,
  452. URL: global.URL,
  453. URLSearchParams: global.URLSearchParams,
  454. WebAssembly: global.WebAssembly,
  455. WritableStream: global.WritableStream,
  456. WritableStreamDefaultController: global.WritableStreamDefaultController,
  457. WritableStreamDefaultWriter: global.WritableStreamDefaultWriter
  458. });
  459. const vmScript = new vm.Script(source, { filename: templateWithoutLoaders });
  460. // Evaluate code and cast to string
  461. let newSource;
  462. try {
  463. newSource = vmScript.runInContext(vmContext);
  464. } catch (e) {
  465. return Promise.reject(e);
  466. }
  467. if (typeof newSource === 'object' && newSource.__esModule && newSource.default) {
  468. newSource = newSource.default;
  469. }
  470. return typeof newSource === 'string' || typeof newSource === 'function'
  471. ? Promise.resolve(newSource)
  472. : Promise.reject(new Error('The loader "' + templateWithoutLoaders + '" didn\'t return html.'));
  473. }
  474. /**
  475. * Add toString methods for easier rendering inside the template
  476. *
  477. * @private
  478. * @param {Array<HtmlTagObject>} assetTagGroup
  479. * @returns {Array<HtmlTagObject>}
  480. */
  481. prepareAssetTagGroupForRendering (assetTagGroup) {
  482. const xhtml = this.options.xhtml;
  483. return HtmlTagArray.from(assetTagGroup.map((assetTag) => {
  484. const copiedAssetTag = Object.assign({}, assetTag);
  485. copiedAssetTag.toString = function () {
  486. return htmlTagObjectToString(this, xhtml);
  487. };
  488. return copiedAssetTag;
  489. }));
  490. }
  491. /**
  492. * Generate the template parameters for the template function
  493. *
  494. * @private
  495. * @param {Compilation} compilation
  496. * @param {AssetsInformationByGroups} assetsInformationByGroups
  497. * @param {{
  498. headTags: HtmlTagObject[],
  499. bodyTags: HtmlTagObject[]
  500. }} assetTags
  501. * @returns {Promise<{[key: any]: any}>}
  502. */
  503. getTemplateParameters (compilation, assetsInformationByGroups, assetTags) {
  504. const templateParameters = this.options.templateParameters;
  505. if (templateParameters === false) {
  506. return Promise.resolve({});
  507. }
  508. if (typeof templateParameters !== 'function' && typeof templateParameters !== 'object') {
  509. throw new Error('templateParameters has to be either a function or an object');
  510. }
  511. const templateParameterFunction = typeof templateParameters === 'function'
  512. // A custom function can overwrite the entire template parameter preparation
  513. ? templateParameters
  514. // If the template parameters is an object merge it with the default values
  515. : (compilation, assetsInformationByGroups, assetTags, options) => Object.assign({},
  516. templateParametersGenerator(compilation, assetsInformationByGroups, assetTags, options),
  517. templateParameters
  518. );
  519. const preparedAssetTags = {
  520. headTags: this.prepareAssetTagGroupForRendering(assetTags.headTags),
  521. bodyTags: this.prepareAssetTagGroupForRendering(assetTags.bodyTags)
  522. };
  523. return Promise
  524. .resolve()
  525. .then(() => templateParameterFunction(compilation, assetsInformationByGroups, preparedAssetTags, this.options));
  526. }
  527. /**
  528. * This function renders the actual html by executing the template function
  529. *
  530. * @private
  531. * @param {(templateParameters) => string | Promise<string>} templateFunction
  532. * @param {AssetsInformationByGroups} assetsInformationByGroups
  533. * @param {{
  534. headTags: HtmlTagObject[],
  535. bodyTags: HtmlTagObject[]
  536. }} assetTags
  537. * @param {Compilation} compilation
  538. * @returns Promise<string>
  539. */
  540. executeTemplate (templateFunction, assetsInformationByGroups, assetTags, compilation) {
  541. // Template processing
  542. const templateParamsPromise = this.getTemplateParameters(compilation, assetsInformationByGroups, assetTags);
  543. return templateParamsPromise.then((templateParams) => {
  544. try {
  545. // If html is a promise return the promise
  546. // If html is a string turn it into a promise
  547. return templateFunction(templateParams);
  548. } catch (e) {
  549. compilation.errors.push(new Error('Template execution failed: ' + e));
  550. return Promise.reject(e);
  551. }
  552. });
  553. }
  554. /**
  555. * Html Post processing
  556. *
  557. * @private
  558. * @param {Compiler} compiler The compiler instance
  559. * @param {any} originalHtml The input html
  560. * @param {AssetsInformationByGroups} assetsInformationByGroups
  561. * @param {{headTags: HtmlTagObject[], bodyTags: HtmlTagObject[]}} assetTags The asset tags to inject
  562. * @returns {Promise<string>}
  563. */
  564. postProcessHtml (compiler, originalHtml, assetsInformationByGroups, assetTags) {
  565. let html = originalHtml;
  566. if (typeof html !== 'string') {
  567. return Promise.reject(new Error('Expected html to be a string but got ' + JSON.stringify(html)));
  568. }
  569. if (this.options.inject) {
  570. const htmlRegExp = /(<html[^>]*>)/i;
  571. const headRegExp = /(<\/head\s*>)/i;
  572. const bodyRegExp = /(<\/body\s*>)/i;
  573. const metaViewportRegExp = /<meta[^>]+name=["']viewport["'][^>]*>/i;
  574. const body = assetTags.bodyTags.map((assetTagObject) => htmlTagObjectToString(assetTagObject, this.options.xhtml));
  575. const head = assetTags.headTags.filter((item) => {
  576. if (item.tagName === 'meta' && item.attributes && item.attributes.name === 'viewport' && metaViewportRegExp.test(html)) {
  577. return false;
  578. }
  579. return true;
  580. }).map((assetTagObject) => htmlTagObjectToString(assetTagObject, this.options.xhtml));
  581. if (body.length) {
  582. if (bodyRegExp.test(html)) {
  583. // Append assets to body element
  584. html = html.replace(bodyRegExp, match => body.join('') + match);
  585. } else {
  586. // Append scripts to the end of the file if no <body> element exists:
  587. html += body.join('');
  588. }
  589. }
  590. if (head.length) {
  591. // Create a head tag if none exists
  592. if (!headRegExp.test(html)) {
  593. if (!htmlRegExp.test(html)) {
  594. html = '<head></head>' + html;
  595. } else {
  596. html = html.replace(htmlRegExp, match => match + '<head></head>');
  597. }
  598. }
  599. // Append assets to head element
  600. html = html.replace(headRegExp, match => head.join('') + match);
  601. }
  602. // Inject manifest into the opening html tag
  603. if (assetsInformationByGroups.manifest) {
  604. html = html.replace(/(<html[^>]*)(>)/i, (match, start, end) => {
  605. // Append the manifest only if no manifest was specified
  606. if (/\smanifest\s*=/.test(match)) {
  607. return match;
  608. }
  609. return start + ' manifest="' + assetsInformationByGroups.manifest + '"' + end;
  610. });
  611. }
  612. }
  613. // TODO avoid this logic and use https://github.com/webpack-contrib/html-minimizer-webpack-plugin under the hood in the next major version
  614. // Check if webpack is running in production mode
  615. // @see https://github.com/webpack/webpack/blob/3366421f1784c449f415cda5930a8e445086f688/lib/WebpackOptionsDefaulter.js#L12-L14
  616. const isProductionLikeMode = compiler.options.mode === 'production' || !compiler.options.mode;
  617. const needMinify = this.options.minify === true || typeof this.options.minify === 'object' || (this.options.minify === 'auto' && isProductionLikeMode);
  618. if (!needMinify) {
  619. return Promise.resolve(html);
  620. }
  621. const minifyOptions = typeof this.options.minify === 'object'
  622. ? this.options.minify
  623. : {
  624. // https://www.npmjs.com/package/html-minifier-terser#options-quick-reference
  625. collapseWhitespace: true,
  626. keepClosingSlash: true,
  627. removeComments: true,
  628. removeRedundantAttributes: true,
  629. removeScriptTypeAttributes: true,
  630. removeStyleLinkTypeAttributes: true,
  631. useShortDoctype: true
  632. };
  633. try {
  634. html = require('html-minifier-terser').minify(html, minifyOptions);
  635. } catch (e) {
  636. const isParseError = String(e.message).indexOf('Parse Error') === 0;
  637. if (isParseError) {
  638. e.message = 'html-webpack-plugin could not minify the generated output.\n' +
  639. 'In production mode the html minifcation is enabled by default.\n' +
  640. 'If you are not generating a valid html output please disable it manually.\n' +
  641. 'You can do so by adding the following setting to your HtmlWebpackPlugin config:\n|\n|' +
  642. ' minify: false\n|\n' +
  643. 'See https://github.com/jantimon/html-webpack-plugin#options for details.\n\n' +
  644. 'For parser dedicated bugs please create an issue here:\n' +
  645. 'https://danielruf.github.io/html-minifier-terser/' +
  646. '\n' + e.message;
  647. }
  648. return Promise.reject(e);
  649. }
  650. return Promise.resolve(html);
  651. }
  652. /**
  653. * Helper to return a sorted unique array of all asset files out of the asset object
  654. * @private
  655. */
  656. getAssetFiles (assets) {
  657. const files = _.uniq(Object.keys(assets).filter(assetType => assetType !== 'chunks' && assets[assetType]).reduce((files, assetType) => files.concat(assets[assetType]), []));
  658. files.sort();
  659. return files;
  660. }
  661. /**
  662. * Converts a favicon file from disk to a webpack resource and returns the url to the resource
  663. *
  664. * @private
  665. * @param {Compiler} compiler
  666. * @param {string|false} favicon
  667. * @param {Compilation} compilation
  668. * @param {string} publicPath
  669. * @param {PreviousEmittedAssets} previousEmittedAssets
  670. * @returns {Promise<string|undefined>}
  671. */
  672. generateFavicon (compiler, favicon, compilation, publicPath, previousEmittedAssets) {
  673. if (!favicon) {
  674. return Promise.resolve(undefined);
  675. }
  676. const filename = path.resolve(compilation.compiler.context, favicon);
  677. return promisify(compilation.inputFileSystem.readFile)(filename)
  678. .then((buf) => {
  679. const source = new compiler.webpack.sources.RawSource(/** @type {string | Buffer} */ (buf), false);
  680. const name = path.basename(filename);
  681. compilation.fileDependencies.add(filename);
  682. compilation.emitAsset(name, source);
  683. previousEmittedAssets.push({ name, source });
  684. const faviconPath = publicPath + name;
  685. if (this.options.hash) {
  686. return this.appendHash(faviconPath, /** @type {string} */ (compilation.hash));
  687. }
  688. return faviconPath;
  689. })
  690. .catch(() => Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename)));
  691. }
  692. /**
  693. * Generate all tags script for the given file paths
  694. *
  695. * @private
  696. * @param {Array<string>} jsAssets
  697. * @returns {Array<HtmlTagObject>}
  698. */
  699. generatedScriptTags (jsAssets) {
  700. // @ts-ignore
  701. return jsAssets.map(src => {
  702. const attributes = {};
  703. if (this.options.scriptLoading === 'defer') {
  704. attributes.defer = true;
  705. } else if (this.options.scriptLoading === 'module') {
  706. attributes.type = 'module';
  707. } else if (this.options.scriptLoading === 'systemjs-module') {
  708. attributes.type = 'systemjs-module';
  709. }
  710. attributes.src = src;
  711. return {
  712. tagName: 'script',
  713. voidTag: false,
  714. meta: { plugin: 'html-webpack-plugin' },
  715. attributes
  716. };
  717. });
  718. }
  719. /**
  720. * Generate all style tags for the given file paths
  721. *
  722. * @private
  723. * @param {Array<string>} cssAssets
  724. * @returns {Array<HtmlTagObject>}
  725. */
  726. generateStyleTags (cssAssets) {
  727. return cssAssets.map(styleAsset => ({
  728. tagName: 'link',
  729. voidTag: true,
  730. meta: { plugin: 'html-webpack-plugin' },
  731. attributes: {
  732. href: styleAsset,
  733. rel: 'stylesheet'
  734. }
  735. }));
  736. }
  737. /**
  738. * Generate an optional base tag
  739. *
  740. * @param {string | {[attributeName: string]: string}} base
  741. * @returns {Array<HtmlTagObject>}
  742. */
  743. generateBaseTag (base) {
  744. return [{
  745. tagName: 'base',
  746. voidTag: true,
  747. meta: { plugin: 'html-webpack-plugin' },
  748. // attributes e.g. { href:"http://example.com/page.html" target:"_blank" }
  749. attributes: typeof base === 'string' ? {
  750. href: base
  751. } : base
  752. }];
  753. }
  754. /**
  755. * Generate all meta tags for the given meta configuration
  756. *
  757. * @private
  758. * @param {false | {[name: string]: false | string | {[attributeName: string]: string|boolean}}} metaOptions
  759. * @returns {Array<HtmlTagObject>}
  760. */
  761. generatedMetaTags (metaOptions) {
  762. if (metaOptions === false) {
  763. return [];
  764. }
  765. // Make tags self-closing in case of xhtml
  766. // Turn { "viewport" : "width=500, initial-scale=1" } into
  767. // [{ name:"viewport" content:"width=500, initial-scale=1" }]
  768. const metaTagAttributeObjects = Object.keys(metaOptions)
  769. .map((metaName) => {
  770. const metaTagContent = metaOptions[metaName];
  771. return (typeof metaTagContent === 'string') ? {
  772. name: metaName,
  773. content: metaTagContent
  774. } : metaTagContent;
  775. })
  776. .filter((attribute) => attribute !== false);
  777. // Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
  778. // the html-webpack-plugin tag structure
  779. return metaTagAttributeObjects.map((metaTagAttributes) => {
  780. if (metaTagAttributes === false) {
  781. throw new Error('Invalid meta tag');
  782. }
  783. return {
  784. tagName: 'meta',
  785. voidTag: true,
  786. meta: { plugin: 'html-webpack-plugin' },
  787. attributes: metaTagAttributes
  788. };
  789. });
  790. }
  791. /**
  792. * Generate a favicon tag for the given file path
  793. *
  794. * @private
  795. * @param {string} favicon
  796. * @returns {Array<HtmlTagObject>}
  797. */
  798. generateFaviconTag (favicon) {
  799. return [{
  800. tagName: 'link',
  801. voidTag: true,
  802. meta: { plugin: 'html-webpack-plugin' },
  803. attributes: {
  804. rel: 'icon',
  805. href: favicon
  806. }
  807. }];
  808. }
  809. /**
  810. * Group assets to head and body tags
  811. *
  812. * @param {{
  813. scripts: Array<HtmlTagObject>;
  814. styles: Array<HtmlTagObject>;
  815. meta: Array<HtmlTagObject>;
  816. }} assetTags
  817. * @param {"body" | "head"} scriptTarget
  818. * @returns {{
  819. headTags: Array<HtmlTagObject>;
  820. bodyTags: Array<HtmlTagObject>;
  821. }}
  822. */
  823. groupAssetsByElements (assetTags, scriptTarget) {
  824. /** @type {{ headTags: Array<HtmlTagObject>; bodyTags: Array<HtmlTagObject>; }} */
  825. const result = {
  826. headTags: [
  827. ...assetTags.meta,
  828. ...assetTags.styles
  829. ],
  830. bodyTags: []
  831. };
  832. // Add script tags to head or body depending on
  833. // the htmlPluginOptions
  834. if (scriptTarget === 'body') {
  835. result.bodyTags.push(...assetTags.scripts);
  836. } else {
  837. // If script loading is blocking add the scripts to the end of the head
  838. // If script loading is non-blocking add the scripts in front of the css files
  839. const insertPosition = this.options.scriptLoading === 'blocking' ? result.headTags.length : assetTags.meta.length;
  840. result.headTags.splice(insertPosition, 0, ...assetTags.scripts);
  841. }
  842. return result;
  843. }
  844. /**
  845. * Replace [contenthash] in filename
  846. *
  847. * @see https://survivejs.com/webpack/optimizing/adding-hashes-to-filenames/
  848. *
  849. * @private
  850. * @param {Compiler} compiler
  851. * @param {string} filename
  852. * @param {string|Buffer} fileContent
  853. * @param {Compilation} compilation
  854. * @returns {{ path: string, info: {} }}
  855. */
  856. replacePlaceholdersInFilename (compiler, filename, fileContent, compilation) {
  857. if (/\[\\*([\w:]+)\\*\]/i.test(filename) === false) {
  858. return { path: filename, info: {} };
  859. }
  860. const hash = compiler.webpack.util.createHash(compilation.outputOptions.hashFunction);
  861. hash.update(fileContent);
  862. if (compilation.outputOptions.hashSalt) {
  863. hash.update(compilation.outputOptions.hashSalt);
  864. }
  865. const contentHash = hash.digest(compilation.outputOptions.hashDigest).slice(0, compilation.outputOptions.hashDigestLength);
  866. return compilation.getPathWithInfo(
  867. filename,
  868. {
  869. contentHash,
  870. chunk: {
  871. hash: contentHash,
  872. contentHash
  873. }
  874. }
  875. );
  876. }
  877. /**
  878. * Function to generate HTML file.
  879. *
  880. * @private
  881. * @param {Compiler} compiler
  882. * @param {Compilation} compilation
  883. * @param {string} outputName
  884. * @param {CachedChildCompilation} childCompilerPlugin
  885. * @param {PreviousEmittedAssets} previousEmittedAssets
  886. * @param {{ value: string | undefined }} assetJson
  887. * @param {(err?: Error) => void} callback
  888. */
  889. generateHTML (
  890. compiler,
  891. compilation,
  892. outputName,
  893. childCompilerPlugin,
  894. previousEmittedAssets,
  895. assetJson,
  896. callback
  897. ) {
  898. // Get all entry point names for this html file
  899. const entryNames = Array.from(compilation.entrypoints.keys());
  900. const filteredEntryNames = this.filterEntryChunks(entryNames, this.options.chunks, this.options.excludeChunks);
  901. const sortedEntryNames = this.sortEntryChunks(filteredEntryNames, this.options.chunksSortMode, compilation);
  902. const templateResult = this.options.templateContent
  903. ? { mainCompilationHash: compilation.hash }
  904. : childCompilerPlugin.getCompilationEntryResult(this.options.template);
  905. if ('error' in templateResult) {
  906. compilation.errors.push(prettyError(templateResult.error, compiler.context).toString());
  907. }
  908. // If the child compilation was not executed during a previous main compile run
  909. // it is a cached result
  910. const isCompilationCached = templateResult.mainCompilationHash !== compilation.hash;
  911. /** Generated file paths from the entry point names */
  912. const assetsInformationByGroups = this.getAssetsInformationByGroups(compilation, outputName, sortedEntryNames);
  913. // If the template and the assets did not change we don't have to emit the html
  914. const newAssetJson = JSON.stringify(this.getAssetFiles(assetsInformationByGroups));
  915. if (isCompilationCached && this.options.cache && assetJson.value === newAssetJson) {
  916. previousEmittedAssets.forEach(({ name, source, info }) => {
  917. compilation.emitAsset(name, source, info);
  918. });
  919. return callback();
  920. } else {
  921. previousEmittedAssets.length = 0;
  922. assetJson.value = newAssetJson;
  923. }
  924. // The html-webpack plugin uses a object representation for the html-tags which will be injected
  925. // to allow altering them more easily
  926. // Just before they are converted a third-party-plugin author might change the order and content
  927. const assetsPromise = this.generateFavicon(compiler, this.options.favicon, compilation, assetsInformationByGroups.publicPath, previousEmittedAssets)
  928. .then((faviconPath) => {
  929. assetsInformationByGroups.favicon = faviconPath;
  930. return getHtmlWebpackPluginHooks(compilation).beforeAssetTagGeneration.promise({
  931. assets: assetsInformationByGroups,
  932. outputName,
  933. plugin: this
  934. });
  935. });
  936. // Turn the js and css paths into grouped HtmlTagObjects
  937. const assetTagGroupsPromise = assetsPromise
  938. // And allow third-party-plugin authors to reorder and change the assetTags before they are grouped
  939. .then(({ assets }) => getHtmlWebpackPluginHooks(compilation).alterAssetTags.promise({
  940. assetTags: {
  941. scripts: this.generatedScriptTags(assets.js),
  942. styles: this.generateStyleTags(assets.css),
  943. meta: [
  944. ...(this.options.base !== false ? this.generateBaseTag(this.options.base) : []),
  945. ...this.generatedMetaTags(this.options.meta),
  946. ...(assets.favicon ? this.generateFaviconTag(assets.favicon) : [])
  947. ]
  948. },
  949. outputName,
  950. publicPath: assetsInformationByGroups.publicPath,
  951. plugin: this
  952. }))
  953. .then(({ assetTags }) => {
  954. // Inject scripts to body unless it set explicitly to head
  955. const scriptTarget = this.options.inject === 'head' ||
  956. (this.options.inject !== 'body' && this.options.scriptLoading !== 'blocking') ? 'head' : 'body';
  957. // Group assets to `head` and `body` tag arrays
  958. const assetGroups = this.groupAssetsByElements(assetTags, scriptTarget);
  959. // Allow third-party-plugin authors to reorder and change the assetTags once they are grouped
  960. return getHtmlWebpackPluginHooks(compilation).alterAssetTagGroups.promise({
  961. headTags: assetGroups.headTags,
  962. bodyTags: assetGroups.bodyTags,
  963. outputName,
  964. publicPath: assetsInformationByGroups.publicPath,
  965. plugin: this
  966. });
  967. });
  968. // Turn the compiled template into a nodejs function or into a nodejs string
  969. const templateEvaluationPromise = Promise.resolve()
  970. .then(() => {
  971. if ('error' in templateResult) {
  972. return this.options.showErrors ? prettyError(templateResult.error, compiler.context).toHtml() : 'ERROR';
  973. }
  974. // Allow to use a custom function / string instead
  975. if (this.options.templateContent !== false) {
  976. return this.options.templateContent;
  977. }
  978. // Once everything is compiled evaluate the html factory and replace it with its content
  979. if ('compiledEntry' in templateResult) {
  980. const compiledEntry = templateResult.compiledEntry;
  981. const assets = compiledEntry.assets;
  982. // Store assets from child compiler to reemit them later
  983. for (const name in assets) {
  984. previousEmittedAssets.push({ name, source: assets[name].source, info: assets[name].info });
  985. }
  986. return this.evaluateCompilationResult(compiledEntry.content, assetsInformationByGroups.publicPath, this.options.template);
  987. }
  988. return Promise.reject(new Error('Child compilation contained no compiledEntry'));
  989. });
  990. const templateExectutionPromise = Promise.all([assetsPromise, assetTagGroupsPromise, templateEvaluationPromise])
  991. // Execute the template
  992. .then(([assetsHookResult, assetTags, compilationResult]) => typeof compilationResult !== 'function'
  993. ? compilationResult
  994. : this.executeTemplate(compilationResult, assetsHookResult.assets, { headTags: assetTags.headTags, bodyTags: assetTags.bodyTags }, compilation));
  995. const injectedHtmlPromise = Promise.all([assetTagGroupsPromise, templateExectutionPromise])
  996. // Allow plugins to change the html before assets are injected
  997. .then(([assetTags, html]) => {
  998. const pluginArgs = { html, headTags: assetTags.headTags, bodyTags: assetTags.bodyTags, plugin: this, outputName };
  999. return getHtmlWebpackPluginHooks(compilation).afterTemplateExecution.promise(pluginArgs);
  1000. })
  1001. .then(({ html, headTags, bodyTags }) => {
  1002. return this.postProcessHtml(compiler, html, assetsInformationByGroups, { headTags, bodyTags });
  1003. });
  1004. const emitHtmlPromise = injectedHtmlPromise
  1005. // Allow plugins to change the html after assets are injected
  1006. .then((html) => {
  1007. const pluginArgs = { html, plugin: this, outputName };
  1008. return getHtmlWebpackPluginHooks(compilation).beforeEmit.promise(pluginArgs)
  1009. .then(result => result.html);
  1010. })
  1011. .catch(err => {
  1012. // In case anything went wrong the promise is resolved
  1013. // with the error message and an error is logged
  1014. compilation.errors.push(prettyError(err, compiler.context).toString());
  1015. return this.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
  1016. })
  1017. .then(html => {
  1018. const filename = outputName.replace(/\[templatehash([^\]]*)\]/g, require('util').deprecate(
  1019. (match, options) => `[contenthash${options}]`,
  1020. '[templatehash] is now [contenthash]')
  1021. );
  1022. const replacedFilename = this.replacePlaceholdersInFilename(compiler, filename, html, compilation);
  1023. const source = new compiler.webpack.sources.RawSource(html, false);
  1024. // Add the evaluated html code to the webpack assets
  1025. compilation.emitAsset(replacedFilename.path, source, replacedFilename.info);
  1026. previousEmittedAssets.push({ name: replacedFilename.path, source });
  1027. return replacedFilename.path;
  1028. })
  1029. .then((finalOutputName) => getHtmlWebpackPluginHooks(compilation).afterEmit.promise({
  1030. outputName: finalOutputName,
  1031. plugin: this
  1032. }).catch(err => {
  1033. /** @type {Logger} */
  1034. (this.logger).error(err);
  1035. return null;
  1036. }).then(() => null));
  1037. // Once all files are added to the webpack compilation
  1038. // let the webpack compiler continue
  1039. emitHtmlPromise.then(() => {
  1040. callback();
  1041. });
  1042. }
  1043. }
  1044. /**
  1045. * The default for options.templateParameter
  1046. * Generate the template parameters
  1047. *
  1048. * Generate the template parameters for the template function
  1049. * @param {Compilation} compilation
  1050. * @param {AssetsInformationByGroups} assets
  1051. * @param {{
  1052. headTags: HtmlTagObject[],
  1053. bodyTags: HtmlTagObject[]
  1054. }} assetTags
  1055. * @param {ProcessedHtmlWebpackOptions} options
  1056. * @returns {TemplateParameter}
  1057. */
  1058. function templateParametersGenerator (compilation, assets, assetTags, options) {
  1059. return {
  1060. compilation: compilation,
  1061. webpackConfig: compilation.options,
  1062. htmlWebpackPlugin: {
  1063. tags: assetTags,
  1064. files: assets,
  1065. options: options
  1066. }
  1067. };
  1068. }
  1069. // Statics:
  1070. /**
  1071. * The major version number of this plugin
  1072. */
  1073. HtmlWebpackPlugin.version = 5;
  1074. /**
  1075. * A static helper to get the hooks for this plugin
  1076. *
  1077. * Usage: HtmlWebpackPlugin.getHooks(compilation).HOOK_NAME.tapAsync('YourPluginName', () => { ... });
  1078. */
  1079. HtmlWebpackPlugin.getHooks = getHtmlWebpackPluginHooks;
  1080. HtmlWebpackPlugin.createHtmlTagObject = createHtmlTagObject;
  1081. module.exports = HtmlWebpackPlugin;