25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 

930 satır
33 KiB

  1. // @ts-check
  2. // Import types
  3. /* eslint-disable */
  4. /// <reference path="./typings.d.ts" />
  5. /* eslint-enable */
  6. /** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */
  7. /** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
  8. 'use strict';
  9. // use Polyfill for util.promisify in node versions < v8
  10. const promisify = require('util.promisify');
  11. const vm = require('vm');
  12. const fs = require('fs');
  13. const _ = require('lodash');
  14. const path = require('path');
  15. const loaderUtils = require('loader-utils');
  16. const { createHtmlTagObject, htmlTagObjectToString } = require('./lib/html-tags');
  17. const childCompiler = require('./lib/compiler.js');
  18. const prettyError = require('./lib/errors.js');
  19. const chunkSorter = require('./lib/chunksorter.js');
  20. const getHtmlWebpackPluginHooks = require('./lib/hooks.js').getHtmlWebpackPluginHooks;
  21. const fsStatAsync = promisify(fs.stat);
  22. const fsReadFileAsync = promisify(fs.readFile);
  23. class HtmlWebpackPlugin {
  24. /**
  25. * @param {Partial<HtmlWebpackPluginOptions>} [options]
  26. */
  27. constructor (options) {
  28. /** @type {Partial<HtmlWebpackPluginOptions>} */
  29. const userOptions = options || {};
  30. // Default options
  31. /** @type {HtmlWebpackPluginOptions} */
  32. const defaultOptions = {
  33. template: path.join(__dirname, 'default_index.ejs'),
  34. templateContent: false,
  35. templateParameters: templateParametersGenerator,
  36. filename: 'index.html',
  37. hash: false,
  38. inject: true,
  39. compile: true,
  40. favicon: false,
  41. minify: false,
  42. cache: true,
  43. showErrors: true,
  44. chunks: 'all',
  45. excludeChunks: [],
  46. chunksSortMode: 'auto',
  47. meta: {},
  48. title: 'Webpack App',
  49. xhtml: false
  50. };
  51. /** @type {HtmlWebpackPluginOptions} */
  52. this.options = Object.assign(defaultOptions, userOptions);
  53. // Default metaOptions if no template is provided
  54. if (!userOptions.template && this.options.templateContent === false && this.options.meta) {
  55. const defaultMeta = {
  56. // From https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag
  57. viewport: 'width=device-width, initial-scale=1'
  58. };
  59. this.options.meta = Object.assign({}, this.options.meta, defaultMeta, userOptions.meta);
  60. }
  61. // Instance variables to keep caching information
  62. // for multiple builds
  63. this.childCompilerHash = undefined;
  64. /**
  65. * @type {string | undefined}
  66. */
  67. this.childCompilationOutputName = undefined;
  68. this.assetJson = undefined;
  69. this.hash = undefined;
  70. this.version = HtmlWebpackPlugin.version;
  71. }
  72. /**
  73. * apply is called by the webpack main compiler during the start phase
  74. * @param {WebpackCompiler} compiler
  75. */
  76. apply (compiler) {
  77. const self = this;
  78. let isCompilationCached = false;
  79. /** @type Promise<string> */
  80. let compilationPromise;
  81. this.options.template = this.getFullTemplatePath(this.options.template, compiler.context);
  82. // convert absolute filename into relative so that webpack can
  83. // generate it at correct location
  84. const filename = this.options.filename;
  85. if (path.resolve(filename) === path.normalize(filename)) {
  86. this.options.filename = path.relative(compiler.options.output.path, filename);
  87. }
  88. // Clear the cache once a new HtmlWebpackPlugin is added
  89. childCompiler.clearCache(compiler);
  90. // Register all HtmlWebpackPlugins instances at the child compiler
  91. compiler.hooks.thisCompilation.tap('HtmlWebpackPlugin', (compilation) => {
  92. // Clear the cache if the child compiler is outdated
  93. if (childCompiler.hasOutDatedTemplateCache(compilation)) {
  94. childCompiler.clearCache(compiler);
  95. }
  96. // Add this instances template to the child compiler
  97. childCompiler.addTemplateToCompiler(compiler, this.options.template);
  98. // Add file dependencies of child compiler to parent compiler
  99. // to keep them watched even if we get the result from the cache
  100. compilation.hooks.additionalChunkAssets.tap('HtmlWebpackPlugin', () => {
  101. const childCompilerDependencies = childCompiler.getFileDependencies(compiler);
  102. childCompilerDependencies.forEach(fileDependency => {
  103. compilation.compilationDependencies.add(fileDependency);
  104. });
  105. });
  106. });
  107. compiler.hooks.make.tapAsync('HtmlWebpackPlugin', (compilation, callback) => {
  108. // Compile the template (queued)
  109. compilationPromise = childCompiler.compileTemplate(self.options.template, self.options.filename, compilation)
  110. .catch(err => {
  111. compilation.errors.push(prettyError(err, compiler.context).toString());
  112. return {
  113. content: self.options.showErrors ? prettyError(err, compiler.context).toJsonHtml() : 'ERROR',
  114. outputName: self.options.filename,
  115. hash: ''
  116. };
  117. })
  118. .then(compilationResult => {
  119. // If the compilation change didnt change the cache is valid
  120. isCompilationCached = Boolean(compilationResult.hash) && self.childCompilerHash === compilationResult.hash;
  121. self.childCompilerHash = compilationResult.hash;
  122. self.childCompilationOutputName = compilationResult.outputName;
  123. callback();
  124. return compilationResult.content;
  125. });
  126. });
  127. compiler.hooks.emit.tapAsync('HtmlWebpackPlugin',
  128. /**
  129. * Hook into the webpack emit phase
  130. * @param {WebpackCompilation} compilation
  131. * @param {() => void} callback
  132. */
  133. (compilation, callback) => {
  134. // Get all entry point names for this html file
  135. const entryNames = Array.from(compilation.entrypoints.keys());
  136. const filteredEntryNames = self.filterChunks(entryNames, self.options.chunks, self.options.excludeChunks);
  137. const sortedEntryNames = self.sortEntryChunks(filteredEntryNames, this.options.chunksSortMode, compilation);
  138. const childCompilationOutputName = self.childCompilationOutputName;
  139. if (childCompilationOutputName === undefined) {
  140. throw new Error('Did not receive child compilation result');
  141. }
  142. // Turn the entry point names into file paths
  143. const assets = self.htmlWebpackPluginAssets(compilation, childCompilationOutputName, sortedEntryNames);
  144. // If this is a hot update compilation, move on!
  145. // This solves a problem where an `index.html` file is generated for hot-update js files
  146. // It only happens in Webpack 2, where hot updates are emitted separately before the full bundle
  147. if (self.isHotUpdateCompilation(assets)) {
  148. return callback();
  149. }
  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.generatedMetaTags(self.options.meta),
  178. ...self.generateFaviconTags(assets.favicon)
  179. ]
  180. },
  181. outputName: childCompilationOutputName,
  182. plugin: self
  183. }))
  184. .then(({assetTags}) => {
  185. // Inject scripts to body unless it set explictly to head
  186. const scriptTarget = self.options.inject === 'head' ? 'head' : 'body';
  187. // Group assets to `head` and `body` tag arrays
  188. const assetGroups = this.generateAssetGroups(assetTags, scriptTarget);
  189. // Allow third-party-plugin authors to reorder and change the assetTags once they are grouped
  190. return getHtmlWebpackPluginHooks(compilation).alterAssetTagGroups.promise({
  191. headTags: assetGroups.headTags,
  192. bodyTags: assetGroups.bodyTags,
  193. outputName: childCompilationOutputName,
  194. plugin: self
  195. });
  196. });
  197. // Turn the compiled tempalte into a nodejs function or into a nodejs string
  198. const templateEvaluationPromise = compilationPromise
  199. .then(compiledTemplate => {
  200. // Allow to use a custom function / string instead
  201. if (self.options.templateContent !== false) {
  202. return self.options.templateContent;
  203. }
  204. // Once everything is compiled evaluate the html factory
  205. // and replace it with its content
  206. return self.evaluateCompilationResult(compilation, compiledTemplate);
  207. });
  208. const templateExectutionPromise = Promise.all([assetsPromise, assetTagGroupsPromise, templateEvaluationPromise])
  209. // Execute the template
  210. .then(([assetsHookResult, assetTags, compilationResult]) => typeof compilationResult !== 'function'
  211. ? compilationResult
  212. : self.executeTemplate(compilationResult, assetsHookResult.assets, { headTags: assetTags.headTags, bodyTags: assetTags.bodyTags }, compilation));
  213. const injectedHtmlPromise = Promise.all([assetTagGroupsPromise, templateExectutionPromise])
  214. // Allow plugins to change the html before assets are injected
  215. .then(([assetTags, html]) => {
  216. const pluginArgs = {html, headTags: assetTags.headTags, bodyTags: assetTags.bodyTags, plugin: self, outputName: childCompilationOutputName};
  217. return getHtmlWebpackPluginHooks(compilation).afterTemplateExecution.promise(pluginArgs);
  218. })
  219. .then(({html, headTags, bodyTags}) => {
  220. return self.postProcessHtml(html, assets, {headTags, bodyTags});
  221. });
  222. const emitHtmlPromise = injectedHtmlPromise
  223. // Allow plugins to change the html after assets are injected
  224. .then((html) => {
  225. const pluginArgs = {html, plugin: self, outputName: childCompilationOutputName};
  226. return getHtmlWebpackPluginHooks(compilation).beforeEmit.promise(pluginArgs)
  227. .then(result => result.html);
  228. })
  229. .catch(err => {
  230. // In case anything went wrong the promise is resolved
  231. // with the error message and an error is logged
  232. compilation.errors.push(prettyError(err, compiler.context).toString());
  233. // Prevent caching
  234. self.hash = null;
  235. return self.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
  236. })
  237. .then(html => {
  238. // Allow to use [contenthash] as placeholder for the html-webpack-plugin name
  239. // See also https://survivejs.com/webpack/optimizing/adding-hashes-to-filenames/
  240. // From https://github.com/webpack-contrib/extract-text-webpack-plugin/blob/8de6558e33487e7606e7cd7cb2adc2cccafef272/src/index.js#L212-L214
  241. const finalOutputName = childCompilationOutputName.replace(/\[(?:(\w+):)?contenthash(?::([a-z]+\d*))?(?::(\d+))?\]/ig, (_, hashType, digestType, maxLength) => {
  242. return loaderUtils.getHashDigest(Buffer.from(html, 'utf8'), hashType, digestType, parseInt(maxLength, 10));
  243. });
  244. // Add the evaluated html code to the webpack assets
  245. compilation.assets[finalOutputName] = {
  246. source: () => html,
  247. size: () => html.length
  248. };
  249. return finalOutputName;
  250. })
  251. .then((finalOutputName) => getHtmlWebpackPluginHooks(compilation).afterEmit.promise({
  252. outputName: finalOutputName,
  253. plugin: self
  254. }).catch(err => {
  255. console.error(err);
  256. return null;
  257. }).then(() => null));
  258. // Once all files are added to the webpack compilation
  259. // let the webpack compiler continue
  260. emitHtmlPromise.then(() => {
  261. callback();
  262. });
  263. });
  264. }
  265. /**
  266. * Evaluates the child compilation result
  267. * @param {WebpackCompilation} compilation
  268. * @param {string} source
  269. * @returns {Promise<string | (() => string | Promise<string>)>}
  270. */
  271. evaluateCompilationResult (compilation, source) {
  272. if (!source) {
  273. return Promise.reject('The child compilation didn\'t provide a result');
  274. }
  275. // The LibraryTemplatePlugin stores the template result in a local variable.
  276. // To extract the result during the evaluation this part has to be removed.
  277. source = source.replace('var HTML_WEBPACK_PLUGIN_RESULT =', '');
  278. const template = this.options.template.replace(/^.+!/, '').replace(/\?.+$/, '');
  279. const vmContext = vm.createContext(_.extend({HTML_WEBPACK_PLUGIN: true, require: require}, global));
  280. const vmScript = new vm.Script(source, {filename: template});
  281. // Evaluate code and cast to string
  282. let newSource;
  283. try {
  284. newSource = vmScript.runInContext(vmContext);
  285. } catch (e) {
  286. return Promise.reject(e);
  287. }
  288. if (typeof newSource === 'object' && newSource.__esModule && newSource.default) {
  289. newSource = newSource.default;
  290. }
  291. return typeof newSource === 'string' || typeof newSource === 'function'
  292. ? Promise.resolve(newSource)
  293. : Promise.reject('The loader "' + this.options.template + '" didn\'t return html.');
  294. }
  295. /**
  296. * Generate the template parameters for the template function
  297. * @param {WebpackCompilation} compilation
  298. * @param {{
  299. publicPath: string,
  300. js: Array<string>,
  301. css: Array<string>,
  302. manifest?: string,
  303. favicon?: string
  304. }} assets
  305. * @param {{
  306. headTags: HtmlTagObject[],
  307. bodyTags: HtmlTagObject[]
  308. }} assetTags
  309. * @returns {{[key: any]: any}}
  310. */
  311. getTemplateParameters (compilation, assets, assetTags) {
  312. if (this.options.templateParameters === false) {
  313. return {};
  314. }
  315. if (typeof this.options.templateParameters === 'function') {
  316. return this.options.templateParameters(compilation, assets, assetTags, this.options);
  317. }
  318. if (typeof this.options.templateParameters === 'object') {
  319. return this.options.templateParameters;
  320. }
  321. throw new Error('templateParameters has to be either a function or an object');
  322. }
  323. /**
  324. * This function renders the actual html by executing the template function
  325. *
  326. * @param {(templatePArameters) => string | Promise<string>} templateFunction
  327. * @param {{
  328. publicPath: string,
  329. js: Array<string>,
  330. css: Array<string>,
  331. manifest?: string,
  332. favicon?: string
  333. }} assets
  334. * @param {{
  335. headTags: HtmlTagObject[],
  336. bodyTags: HtmlTagObject[]
  337. }} assetTags
  338. * @param {WebpackCompilation} compilation
  339. *
  340. * @returns Promise<string>
  341. */
  342. executeTemplate (templateFunction, assets, assetTags, compilation) {
  343. // Template processing
  344. const templateParams = this.getTemplateParameters(compilation, assets, assetTags);
  345. /** @type {string|Promise<string>} */
  346. let html = '';
  347. try {
  348. html = templateFunction(templateParams);
  349. } catch (e) {
  350. compilation.errors.push(new Error('Template execution failed: ' + e));
  351. return Promise.reject(e);
  352. }
  353. // If html is a promise return the promise
  354. // If html is a string turn it into a promise
  355. return Promise.resolve().then(() => html);
  356. }
  357. /**
  358. * Html Post processing
  359. *
  360. * @param {any} html
  361. * The input html
  362. * @param {any} assets
  363. * @param {{
  364. headTags: HtmlTagObject[],
  365. bodyTags: HtmlTagObject[]
  366. }} assetTags
  367. * The asset tags to inject
  368. *
  369. * @returns {Promise<string>}
  370. */
  371. postProcessHtml (html, assets, assetTags) {
  372. if (typeof html !== 'string') {
  373. return Promise.reject('Expected html to be a string but got ' + JSON.stringify(html));
  374. }
  375. const htmlAfterInjection = this.options.inject
  376. ? this.injectAssetsIntoHtml(html, assets, assetTags)
  377. : html;
  378. const htmlAfterMinification = this.options.minify
  379. ? require('html-minifier').minify(htmlAfterInjection, this.options.minify === true ? {} : this.options.minify)
  380. : htmlAfterInjection;
  381. return Promise.resolve(htmlAfterMinification);
  382. }
  383. /*
  384. * Pushes the content of the given filename to the compilation assets
  385. * @param {string} filename
  386. * @param {WebpackCompilation} compilation
  387. *
  388. * @returns {string} file basename
  389. */
  390. addFileToAssets (filename, compilation) {
  391. filename = path.resolve(compilation.compiler.context, filename);
  392. return Promise.all([
  393. fsStatAsync(filename),
  394. fsReadFileAsync(filename)
  395. ])
  396. .then(([size, source]) => {
  397. return {
  398. size,
  399. source
  400. };
  401. })
  402. .catch(() => Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename)))
  403. .then(results => {
  404. const basename = path.basename(filename);
  405. compilation.fileDependencies.add(filename);
  406. compilation.assets[basename] = {
  407. source: () => results.source,
  408. size: () => results.size.size
  409. };
  410. return basename;
  411. });
  412. }
  413. /**
  414. * Helper to sort chunks
  415. * @param {string[]} entryNames
  416. * @param {string|((entryNameA: string, entryNameB: string) => number)} sortMode
  417. * @param {WebpackCompilation} compilation
  418. */
  419. sortEntryChunks (entryNames, sortMode, compilation) {
  420. // Custom function
  421. if (typeof sortMode === 'function') {
  422. return entryNames.sort(sortMode);
  423. }
  424. // Check if the given sort mode is a valid chunkSorter sort mode
  425. if (typeof chunkSorter[sortMode] !== 'undefined') {
  426. return chunkSorter[sortMode](entryNames, compilation, this.options);
  427. }
  428. throw new Error('"' + sortMode + '" is not a valid chunk sort mode');
  429. }
  430. /**
  431. * Return all chunks from the compilation result which match the exclude and include filters
  432. * @param {any} chunks
  433. * @param {string[]|'all'} includedChunks
  434. * @param {string[]} excludedChunks
  435. */
  436. filterChunks (chunks, includedChunks, excludedChunks) {
  437. return chunks.filter(chunkName => {
  438. // Skip if the chunks should be filtered and the given chunk was not added explicity
  439. if (Array.isArray(includedChunks) && includedChunks.indexOf(chunkName) === -1) {
  440. return false;
  441. }
  442. // Skip if the chunks should be filtered and the given chunk was excluded explicity
  443. if (Array.isArray(excludedChunks) && excludedChunks.indexOf(chunkName) !== -1) {
  444. return false;
  445. }
  446. // Add otherwise
  447. return true;
  448. });
  449. }
  450. /**
  451. * Check if the given asset object consists only of hot-update.js files
  452. *
  453. * @param {{
  454. publicPath: string,
  455. js: Array<string>,
  456. css: Array<string>,
  457. manifest?: string,
  458. favicon?: string
  459. }} assets
  460. */
  461. isHotUpdateCompilation (assets) {
  462. return assets.js.length && assets.js.every((assetPath) => /\.hot-update\.js$/.test(assetPath));
  463. }
  464. /**
  465. * The htmlWebpackPluginAssets extracts the asset information of a webpack compilation
  466. * for all given entry names
  467. * @param {WebpackCompilation} compilation
  468. * @param {string[]} entryNames
  469. * @returns {{
  470. publicPath: string,
  471. js: Array<string>,
  472. css: Array<string>,
  473. manifest?: string,
  474. favicon?: string
  475. }}
  476. */
  477. htmlWebpackPluginAssets (compilation, childCompilationOutputName, entryNames) {
  478. const compilationHash = compilation.hash;
  479. /**
  480. * @type {string} the configured public path to the asset root
  481. * if a publicPath is set in the current webpack config use it otherwise
  482. * fallback to a realtive path
  483. */
  484. let publicPath = typeof compilation.options.output.publicPath !== 'undefined'
  485. // If a hard coded public path exists use it
  486. ? compilation.mainTemplate.getPublicPath({hash: compilationHash})
  487. // If no public path was set get a relative url path
  488. : path.relative(path.resolve(compilation.options.output.path, path.dirname(childCompilationOutputName)), compilation.options.output.path)
  489. .split(path.sep).join('/');
  490. if (publicPath.length && publicPath.substr(-1, 1) !== '/') {
  491. publicPath += '/';
  492. }
  493. /**
  494. * @type {{
  495. publicPath: string,
  496. js: Array<string>,
  497. css: Array<string>,
  498. manifest?: string,
  499. favicon?: string
  500. }}
  501. */
  502. const assets = {
  503. // The public path
  504. publicPath: publicPath,
  505. // Will contain all js files
  506. js: [],
  507. // Will contain all css files
  508. css: [],
  509. // Will contain the html5 appcache manifest files if it exists
  510. manifest: Object.keys(compilation.assets).find(assetFile => path.extname(assetFile) === '.appcache'),
  511. // Favicon
  512. favicon: undefined
  513. };
  514. // Append a hash for cache busting
  515. if (this.options.hash && assets.manifest) {
  516. assets.manifest = this.appendHash(assets.manifest, compilationHash);
  517. }
  518. // Extract paths to .js and .css files from the current compilation
  519. const entryPointPublicPathMap = {};
  520. const extensionRegexp = /\.(css|js)(\?|$)/;
  521. for (let i = 0; i < entryNames.length; i++) {
  522. const entryName = entryNames[i];
  523. const entryPointFiles = compilation.entrypoints.get(entryName).getFiles();
  524. // Prepend the publicPath and append the hash depending on the
  525. // webpack.output.publicPath and hashOptions
  526. // E.g. bundle.js -> /bundle.js?hash
  527. const entryPointPublicPaths = entryPointFiles
  528. .map(chunkFile => {
  529. const entryPointPublicPath = publicPath + chunkFile;
  530. return this.options.hash
  531. ? this.appendHash(entryPointPublicPath, compilationHash)
  532. : entryPointPublicPath;
  533. });
  534. entryPointPublicPaths.forEach((entryPointPublicPath) => {
  535. const extMatch = extensionRegexp.exec(entryPointPublicPath);
  536. // Skip if the public path is not a .css or .js file
  537. if (!extMatch) {
  538. return;
  539. }
  540. // Skip if this file is already known
  541. // (e.g. because of common chunk optimizations)
  542. if (entryPointPublicPathMap[entryPointPublicPath]) {
  543. return;
  544. }
  545. entryPointPublicPathMap[entryPointPublicPath] = true;
  546. // ext will contain .js or .css
  547. const ext = extMatch[1];
  548. assets[ext].push(entryPointPublicPath);
  549. });
  550. }
  551. return assets;
  552. }
  553. /**
  554. * Converts a favicon file from disk to a webpack ressource
  555. * and returns the url to the ressource
  556. *
  557. * @param {string|false} faviconFilePath
  558. * @param {WebpackCompilation} compilation
  559. * @parma {string} publicPath
  560. * @returns {Promise<string|undefined>}
  561. */
  562. getFaviconPublicPath (faviconFilePath, compilation, publicPath) {
  563. if (!faviconFilePath) {
  564. return Promise.resolve(undefined);
  565. }
  566. return this.addFileToAssets(faviconFilePath, compilation)
  567. .then((faviconName) => {
  568. const faviconPath = publicPath + faviconName;
  569. if (this.options.hash) {
  570. return this.appendHash(faviconPath, compilation.hash);
  571. }
  572. return faviconPath;
  573. });
  574. }
  575. /**
  576. * Generate meta tags
  577. * @returns {HtmlTagObject[]}
  578. */
  579. getMetaTags () {
  580. const metaOptions = this.options.meta;
  581. if (metaOptions === false) {
  582. return [];
  583. }
  584. // Make tags self-closing in case of xhtml
  585. // Turn { "viewport" : "width=500, initial-scale=1" } into
  586. // [{ name:"viewport" content:"width=500, initial-scale=1" }]
  587. const metaTagAttributeObjects = Object.keys(metaOptions)
  588. .map((metaName) => {
  589. const metaTagContent = metaOptions[metaName];
  590. return (typeof metaTagContent === 'string') ? {
  591. name: metaName,
  592. content: metaTagContent
  593. } : metaTagContent;
  594. })
  595. .filter((attribute) => attribute !== false);
  596. // Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
  597. // the html-webpack-plugin tag structure
  598. return metaTagAttributeObjects.map((metaTagAttributes) => {
  599. if (metaTagAttributes === false) {
  600. throw new Error('Invalid meta tag');
  601. }
  602. return {
  603. tagName: 'meta',
  604. voidTag: true,
  605. attributes: metaTagAttributes
  606. };
  607. });
  608. }
  609. /**
  610. * Generate all tags script for the given file paths
  611. * @param {Array<string>} jsAssets
  612. * @returns {Array<HtmlTagObject>}
  613. */
  614. generatedScriptTags (jsAssets) {
  615. return jsAssets.map(scriptAsset => ({
  616. tagName: 'script',
  617. voidTag: false,
  618. attributes: {
  619. src: scriptAsset
  620. }
  621. }));
  622. }
  623. /**
  624. * Generate all style tags for the given file paths
  625. * @param {Array<string>} cssAssets
  626. * @returns {Array<HtmlTagObject>}
  627. */
  628. generateStyleTags (cssAssets) {
  629. return cssAssets.map(styleAsset => ({
  630. tagName: 'link',
  631. voidTag: true,
  632. attributes: {
  633. href: styleAsset,
  634. rel: 'stylesheet'
  635. }
  636. }));
  637. }
  638. /**
  639. * Generate all meta tags for the given meta configuration
  640. * @param {false | {
  641. [name: string]: string|false // name content pair e.g. {viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no'}`
  642. | {[attributeName: string]: string|boolean} // custom properties e.g. { name:"viewport" content:"width=500, initial-scale=1" }
  643. }} metaOptions
  644. * @returns {Array<HtmlTagObject>}
  645. */
  646. generatedMetaTags (metaOptions) {
  647. if (metaOptions === false) {
  648. return [];
  649. }
  650. // Make tags self-closing in case of xhtml
  651. // Turn { "viewport" : "width=500, initial-scale=1" } into
  652. // [{ name:"viewport" content:"width=500, initial-scale=1" }]
  653. const metaTagAttributeObjects = Object.keys(metaOptions)
  654. .map((metaName) => {
  655. const metaTagContent = metaOptions[metaName];
  656. return (typeof metaTagContent === 'string') ? {
  657. name: metaName,
  658. content: metaTagContent
  659. } : metaTagContent;
  660. })
  661. .filter((attribute) => attribute !== false);
  662. // Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
  663. // the html-webpack-plugin tag structure
  664. return metaTagAttributeObjects.map((metaTagAttributes) => {
  665. if (metaTagAttributes === false) {
  666. throw new Error('Invalid meta tag');
  667. }
  668. return {
  669. tagName: 'meta',
  670. voidTag: true,
  671. attributes: metaTagAttributes
  672. };
  673. });
  674. }
  675. /**
  676. * Generate a favicon tag for the given file path
  677. * @param {string| undefined} faviconPath
  678. * @returns {Array<HtmlTagObject>}
  679. */
  680. generateFaviconTags (faviconPath) {
  681. if (!faviconPath) {
  682. return [];
  683. }
  684. return [{
  685. tagName: 'link',
  686. voidTag: true,
  687. attributes: {
  688. rel: 'shortcut icon',
  689. href: faviconPath
  690. }
  691. }];
  692. }
  693. /**
  694. * Group assets to head and bottom tags
  695. *
  696. * @param {{
  697. scripts: Array<HtmlTagObject>;
  698. styles: Array<HtmlTagObject>;
  699. meta: Array<HtmlTagObject>;
  700. }} assetTags
  701. * @param {"body" | "head"} scriptTarget
  702. * @returns {{
  703. headTags: Array<HtmlTagObject>;
  704. bodyTags: Array<HtmlTagObject>;
  705. }}
  706. */
  707. generateAssetGroups (assetTags, scriptTarget) {
  708. /** @type {{ headTags: Array<HtmlTagObject>; bodyTags: Array<HtmlTagObject>; }} */
  709. const result = {
  710. headTags: [
  711. ...assetTags.meta,
  712. ...assetTags.styles
  713. ],
  714. bodyTags: []
  715. };
  716. // Add script tags to head or body depending on
  717. // the htmlPluginOptions
  718. if (scriptTarget === 'body') {
  719. result.bodyTags.push(...assetTags.scripts);
  720. } else {
  721. result.headTags.push(...assetTags.scripts);
  722. }
  723. return result;
  724. }
  725. /**
  726. * Injects the assets into the given html string
  727. *
  728. * @param {string} html
  729. * The input html
  730. * @param {any} assets
  731. * @param {{
  732. headTags: HtmlTagObject[],
  733. bodyTags: HtmlTagObject[]
  734. }} assetTags
  735. * The asset tags to inject
  736. *
  737. * @returns {string}
  738. */
  739. injectAssetsIntoHtml (html, assets, assetTags) {
  740. const htmlRegExp = /(<html[^>]*>)/i;
  741. const headRegExp = /(<\/head\s*>)/i;
  742. const bodyRegExp = /(<\/body\s*>)/i;
  743. const body = assetTags.bodyTags.map((assetTagObject) => htmlTagObjectToString(assetTagObject, this.options.xhtml));
  744. const head = assetTags.headTags.map((assetTagObject) => htmlTagObjectToString(assetTagObject, this.options.xhtml));
  745. if (body.length) {
  746. if (bodyRegExp.test(html)) {
  747. // Append assets to body element
  748. html = html.replace(bodyRegExp, match => body.join('') + match);
  749. } else {
  750. // Append scripts to the end of the file if no <body> element exists:
  751. html += body.join('');
  752. }
  753. }
  754. if (head.length) {
  755. // Create a head tag if none exists
  756. if (!headRegExp.test(html)) {
  757. if (!htmlRegExp.test(html)) {
  758. html = '<head></head>' + html;
  759. } else {
  760. html = html.replace(htmlRegExp, match => match + '<head></head>');
  761. }
  762. }
  763. // Append assets to head element
  764. html = html.replace(headRegExp, match => head.join('') + match);
  765. }
  766. // Inject manifest into the opening html tag
  767. if (assets.manifest) {
  768. html = html.replace(/(<html[^>]*)(>)/i, (match, start, end) => {
  769. // Append the manifest only if no manifest was specified
  770. if (/\smanifest\s*=/.test(match)) {
  771. return match;
  772. }
  773. return start + ' manifest="' + assets.manifest + '"' + end;
  774. });
  775. }
  776. return html;
  777. }
  778. /**
  779. * Appends a cache busting hash to the query string of the url
  780. * E.g. http://localhost:8080/ -> http://localhost:8080/?50c9096ba6183fd728eeb065a26ec175
  781. * @param {string} url
  782. * @param {string} hash
  783. */
  784. appendHash (url, hash) {
  785. if (!url) {
  786. return url;
  787. }
  788. return url + (url.indexOf('?') === -1 ? '?' : '&') + hash;
  789. }
  790. /**
  791. * Helper to return the absolute template path with a fallback loader
  792. * @param {string} template
  793. * The path to the tempalate e.g. './index.html'
  794. * @param {string} context
  795. * The webpack base resolution path for relative paths e.g. process.cwd()
  796. */
  797. getFullTemplatePath (template, context) {
  798. // If the template doesn't use a loader use the lodash template loader
  799. if (template.indexOf('!') === -1) {
  800. template = require.resolve('./lib/loader.js') + '!' + path.resolve(context, template);
  801. }
  802. // Resolve template path
  803. return template.replace(
  804. /([!])([^/\\][^!?]+|[^/\\!?])($|\?[^!?\n]+$)/,
  805. (match, prefix, filepath, postfix) => prefix + path.resolve(filepath) + postfix);
  806. }
  807. /**
  808. * Helper to return a sorted unique array of all asset files out of the
  809. * asset object
  810. */
  811. getAssetFiles (assets) {
  812. const files = _.uniq(Object.keys(assets).filter(assetType => assetType !== 'chunks' && assets[assetType]).reduce((files, assetType) => files.concat(assets[assetType]), []));
  813. files.sort();
  814. return files;
  815. }
  816. }
  817. /**
  818. * The default for options.templateParameter
  819. * Generate the template parameters
  820. *
  821. * Generate the template parameters for the template function
  822. * @param {WebpackCompilation} compilation
  823. * @param {{
  824. publicPath: string,
  825. js: Array<string>,
  826. css: Array<string>,
  827. manifest?: string,
  828. favicon?: string
  829. }} assets
  830. * @param {{
  831. headTags: HtmlTagObject[],
  832. bodyTags: HtmlTagObject[]
  833. }} assetTags
  834. * @param {HtmlWebpackPluginOptions} options
  835. * @returns {HtmlWebpackPluginTemplateParameter}
  836. */
  837. function templateParametersGenerator (compilation, assets, assetTags, options) {
  838. const xhtml = options.xhtml;
  839. assetTags.headTags.toString = function () {
  840. return this.map((assetTagObject) => htmlTagObjectToString(assetTagObject, xhtml)).join('');
  841. };
  842. assetTags.bodyTags.toString = function () {
  843. return this.map((assetTagObject) => htmlTagObjectToString(assetTagObject, xhtml)).join('');
  844. };
  845. return {
  846. compilation: compilation,
  847. webpackConfig: compilation.options,
  848. htmlWebpackPlugin: {
  849. tags: assetTags,
  850. files: assets,
  851. options: options
  852. }
  853. };
  854. }
  855. // Statics:
  856. /**
  857. * The major version number of this plugin
  858. */
  859. HtmlWebpackPlugin.version = 4;
  860. /**
  861. * A static helper to get the hooks for this plugin
  862. *
  863. * Usage: HtmlWebpackPlugin.getHooks(compilation).HOOK_NAME.tapAsync('YourPluginName', () => { ... });
  864. */
  865. HtmlWebpackPlugin.getHooks = getHtmlWebpackPluginHooks;
  866. HtmlWebpackPlugin.createHtmlTagObject = createHtmlTagObject;
  867. module.exports = HtmlWebpackPlugin;