index.js 40 KB

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