code:
// @ts-check
'use strict';

const promisify = require('util').promisify;

const vm = require('vm');
const fs = require('fs');
const _ = require('lodash');
const path = require('path');
const { CachedChildCompilation } = require('./lib/cached-child-compiler');

const { createHtmlTagObject, htmlTagObjectToString, HtmlTagArray } = require('./lib/html-tags');
const prettyError = require('./lib/errors.js');
const chunkSorter = require('./lib/chunksorter.js');
const getHtmlWebpackPluginHooks = require('./lib/hooks.js').getHtmlWebpackPluginHooks;

/** @typedef {import("./typings").HtmlTagObject} HtmlTagObject */
/** @typedef {import("./typings").Options} HtmlWebpackOptions */
/** @typedef {import("./typings").ProcessedOptions} ProcessedHtmlWebpackOptions */
/** @typedef {import("./typings").TemplateParameter} TemplateParameter */
/** @typedef {import("webpack").Compiler} Compiler */
/** @typedef {ReturnType<Compiler["getInfrastructureLogger"]>} Logger */
/** @typedef {import("webpack/lib/Compilation.js")} Compilation */
/** @typedef {Array<{ name: string, source: import('webpack').sources.Source, info?: import('webpack').AssetInfo }>} PreviousEmittedAssets */
/** @typedef {{ publicPath: string, js: Array<string>, css: Array<string>, manifest?: string, favicon?: string }} AssetsInformationByGroups */

class HtmlWebpackPlugin {
  /**
   * @param {HtmlWebpackOptions} [options]
   */
  constructor (options) {
    /** @type {HtmlWebpackOptions} */
    // TODO remove me in the next major release
    this.userOptions = options || {};
    this.version = HtmlWebpackPlugin.version;

    // Default options
    /** @type {ProcessedHtmlWebpackOptions} */
    const defaultOptions = {
      template: 'auto',
      templateContent: false,
      templateParameters: templateParametersGenerator,
      filename: 'index.html',
      publicPath: this.userOptions.publicPath === undefined ? 'auto' : this.userOptions.publicPath,
      hash: false,
      inject: this.userOptions.scriptLoading === 'blocking' ? 'body' : 'head',
      scriptLoading: 'defer',
      compile: true,
      favicon: false,
      minify: 'auto',
      cache: true,
      showErrors: true,
      chunks: 'all',
      excludeChunks: [],
      chunksSortMode: 'auto',
      meta: {},
      base: false,
      title: 'Webpack App',
      xhtml: false
    };

    /** @type {ProcessedHtmlWebpackOptions} */
    this.options = Object.assign(defaultOptions, this.userOptions);
  }

  /**
   *
   * @param {Compiler} compiler
   * @returns {void}
   */
  apply (compiler) {
    this.logger = compiler.getInfrastructureLogger('HtmlWebpackPlugin');

    // Wait for configuration preset plugions to apply all configure webpack defaults
    compiler.hooks.initialize.tap('HtmlWebpackPlugin', () => {
      const options = this.options;

      options.template = this.getTemplatePath(this.options.template, compiler.context);

      // Assert correct option spelling
      if (options.scriptLoading !== 'defer' && options.scriptLoading !== 'blocking' && options.scriptLoading !== 'module' && options.scriptLoading !== 'systemjs-module') {
        /** @type {Logger} */
        (this.logger).error('The "scriptLoading" option need to be set to "defer", "blocking" or "module" or "systemjs-module"');
      }

      if (options.inject !== true && options.inject !== false && options.inject !== 'head' && options.inject !== 'body') {
        /** @type {Logger} */
        (this.logger).error('The `inject` option needs to be set to true, false, "head" or "body');
      }

      if (
        this.options.templateParameters !== false &&
        typeof this.options.templateParameters !== 'function' &&
        typeof this.options.templateParameters !== 'object'
      ) {
        /** @type {Logger} */
        (this.logger).error('The `templateParameters` has to be either a function or an object or false');
      }

      // Default metaOptions if no template is provided
      if (!this.userOptions.template && options.templateContent === false && options.meta) {
        options.meta = Object.assign(
          {},
          options.meta,
          {
            // TODO remove in the next major release
            // From https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag
            viewport: 'width=device-width, initial-scale=1'
          },
          this.userOptions.meta
        );
      }

      // entryName to fileName conversion function
      const userOptionFilename = this.userOptions.filename || this.options.filename;
      const filenameFunction = typeof userOptionFilename === 'function'
        ? userOptionFilename
        // Replace '[name]' with entry name
        : (entryName) => userOptionFilename.replace(/\[name\]/g, entryName);

      /** output filenames for the given entry names */
      const entryNames = Object.keys(compiler.options.entry);
      const outputFileNames = new Set((entryNames.length ? entryNames : ['main']).map(filenameFunction));

      // Hook all options into the webpack compiler
      outputFileNames.forEach((outputFileName) => {
        // Instance variables to keep caching information for multiple builds
        const assetJson = { value: undefined };
        /**
         * store the previous generated asset to emit them even if the content did not change
         * to support watch mode for third party plugins like the clean-webpack-plugin or the compression plugin
         * @type {PreviousEmittedAssets}
         */
        const previousEmittedAssets = [];

        // Inject child compiler plugin
        const childCompilerPlugin = new CachedChildCompilation(compiler);

        if (!this.options.templateContent) {
          childCompilerPlugin.addEntry(this.options.template);
        }

        // convert absolute filename into relative so that webpack can
        // generate it at correct location
        let filename = outputFileName;

        if (path.resolve(filename) === path.normalize(filename)) {
          const outputPath = /** @type {string} - Once initialized the path is always a string */(compiler.options.output.path);

          filename = path.relative(outputPath, filename);
        }

        compiler.hooks.thisCompilation.tap('HtmlWebpackPlugin',
          /**
           * Hook into the webpack compilation
           * @param {Compilation} compilation
           */
          (compilation) => {
            compilation.hooks.processAssets.tapAsync(
              {
                name: 'HtmlWebpackPlugin',
                stage:
                /**
                 * Generate the html after minification and dev tooling is done
                 */
                compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE
              },
              /**
               * Hook into the process assets hook
               * @param {any} _
               * @param {(err?: Error) => void} callback
               */
              (_, callback) => {
                this.generateHTML(compiler, compilation, filename, childCompilerPlugin, previousEmittedAssets, assetJson, callback);
              });
          });
      });
    });
  }

  /**
   * Helper to return the absolute template path with a fallback loader
   *
   * @private
   * @param {string} template The path to the template e.g. './index.html'
   * @param {string} context The webpack base resolution path for relative paths e.g. process.cwd()
   */
  getTemplatePath (template, context) {
    if (template === 'auto') {
      template = path.resolve(context, 'src/index.ejs');
      if (!fs.existsSync(template)) {
        template = path.join(__dirname, 'default_index.ejs');
      }
    }

    // If the template doesn't use a loader use the lodash template loader
    if (template.indexOf('!') === -1) {
      template = require.resolve('./lib/loader.js') + '!' + path.resolve(context, template);
    }

    // Resolve template path
    return template.replace(
      /([!])([^/\\][^!?]+|[^/\\!?])($|\?[^!?\n]+$)/,
      (match, prefix, filepath, postfix) => prefix + path.resolve(filepath) + postfix);
  }

  /**
   * Return all chunks from the compilation result which match the exclude and include filters
   *
   * @private
   * @param {any} chunks
   * @param {string[]|'all'} includedChunks
   * @param {string[]} excludedChunks
   */
  filterEntryChunks (chunks, includedChunks, excludedChunks) {
    return chunks.filter(chunkName => {
      // Skip if the chunks should be filtered and the given chunk was not added explicity
      if (Array.isArray(includedChunks) && includedChunks.indexOf(chunkName) === -1) {
        return false;
      }

      // Skip if the chunks should be filtered and the given chunk was excluded explicity
      if (Array.isArray(excludedChunks) && excludedChunks.indexOf(chunkName) !== -1) {
        return false;
      }

      // Add otherwise
      return true;
    });
  }

  /**
   * Helper to sort chunks
   *
   * @private
   * @param {string[]} entryNames
   * @param {string|((entryNameA: string, entryNameB: string) => number)} sortMode
   * @param {Compilation} compilation
   */
  sortEntryChunks (entryNames, sortMode, compilation) {
    // Custom function
    if (typeof sortMode === 'function') {
      return entryNames.sort(sortMode);
    }
    // Check if the given sort mode is a valid chunkSorter sort mode
    if (typeof chunkSorter[sortMode] !== 'undefined') {
      return chunkSorter[sortMode](entryNames, compilation, this.options);
    }
    throw new Error('"' + sortMode + '" is not a valid chunk sort mode');
  }

  /**
   * Encode each path component using `encodeURIComponent` as files can contain characters
   * which needs special encoding in URLs like `+ `.
   *
   * Valid filesystem characters which need to be encoded for urls:
   *
   * # pound, % percent, & ampersand, { left curly bracket, } right curly bracket,
   * \ back slash, < left angle bracket, > right angle bracket, * asterisk, ? question mark,
   * blank spaces, $ dollar sign, ! exclamation point, ' single quotes, " double quotes,
   * : colon, @ at sign, + plus sign, ` backtick, | pipe, = equal sign
   *
   * However the query string must not be encoded:
   *
   *  fo:demonstration-path/very fancy+name.js?path=/home?value=abc&value=def#zzz
   *    ^             ^    ^    ^     ^    ^  ^    ^^    ^     ^   ^     ^   ^
   *    |             |    |    |     |    |  |    ||    |     |   |     |   |
   *    encoded       |    |    encoded    |  |    ||    |     |   |     |   |
   *                 ignored              ignored  ignored     ignored   ignored
   *
   * @private
   * @param {string} filePath
   */
  urlencodePath (filePath) {
    // People use the filepath in quite unexpected ways.
    // Try to extract the first querystring of the url:
    //
    // some+path/demo.html?value=abc?def
    //
    const queryStringStart = filePath.indexOf('?');
    const urlPath = queryStringStart === -1 ? filePath : filePath.substr(0, queryStringStart);
    const queryString = filePath.substr(urlPath.length);
    // Encode all parts except '/' which are not part of the querystring:
    const encodedUrlPath = urlPath.split('/').map(encodeURIComponent).join('/');
    return encodedUrlPath + queryString;
  }

  /**
   * Appends a cache busting hash to the query string of the url
   * E.g. http://localhost:8080/ -> http://localhost:8080/?50c9096ba6183fd728eeb065a26ec175
   *
   * @private
   * @param {string} url
   * @param {string} hash
   */
  appendHash (url, hash) {
    if (!url) {
      return url;
    }
    return url + (url.indexOf('?') === -1 ? '?' : '&') + hash;
  }

  /**
   * Generate the relative or absolute base url to reference images, css, and javascript files
   * from within the html file - the publicPath
   *
   * @private
   * @param {Compilation} compilation
   * @param {string} filename
   * @param {string | 'auto'} customPublicPath
   * @returns {string}
   */
  getPublicPath (compilation, filename, customPublicPath) {
    /**
     * @type {string} the configured public path to the asset root
     * if a path publicPath is set in the current webpack config use it otherwise
     * fallback to a relative path
     */
    const webpackPublicPath = compilation.getAssetPath(compilation.outputOptions.publicPath, { hash: compilation.hash });
    // Webpack 5 introduced "auto" as default value
    const isPublicPathDefined = webpackPublicPath !== 'auto';

    let publicPath =
      // If the html-webpack-plugin options contain a custom public path uset it
      customPublicPath !== 'auto'
        ? customPublicPath
        : (isPublicPathDefined
          // If a hard coded public path exists use it
          ? webpackPublicPath
          // If no public path was set get a relative url path
          : path.relative(path.resolve(compilation.options.output.path, path.dirname(filename)), compilation.options.output.path)
            .split(path.sep).join('/')
        );

    if (publicPath.length && publicPath.substr(-1, 1) !== '/') {
      publicPath += '/';
    }

    return publicPath;
  }

  /**
   * The getAssetsForHTML extracts the asset information of a webpack compilation for all given entry names.
   *
   * @private
   * @param {Compilation} compilation
   * @param {string} outputName
   * @param {string[]} entryNames
   * @returns {AssetsInformationByGroups}
   */
  getAssetsInformationByGroups (compilation, outputName, entryNames) {
    /** The public path used inside the html file */
    const publicPath = this.getPublicPath(compilation, outputName, this.options.publicPath);
    /**
     * @type {AssetsInformationByGroups}
     */
    const assets = {
      // The public path
      publicPath,
      // Will contain all js and mjs files
      js: [],
      // Will contain all css files
      css: [],
      // Will contain the html5 appcache manifest files if it exists
      manifest: Object.keys(compilation.assets).find(assetFile => path.extname(assetFile) === '.appcache'),
      // Favicon
      favicon: undefined
    };

    // Append a hash for cache busting
    if (this.options.hash && assets.manifest) {
      assets.manifest = this.appendHash(assets.manifest, /** @type {string} */ (compilation.hash));
    }

    // Extract paths to .js, .mjs and .css files from the current compilation
    const entryPointPublicPathMap = {};
    const extensionRegexp = /\.(css|js|mjs)(\?|$)/;

    for (let i = 0; i < entryNames.length; i++) {
      const entryName = entryNames[i];
      /** entryPointUnfilteredFiles - also includes hot module update files */
      const entryPointUnfilteredFiles = compilation.entrypoints.get(entryName).getFiles();
      const entryPointFiles = entryPointUnfilteredFiles.filter((chunkFile) => {
        const asset = compilation.getAsset(chunkFile);

        if (!asset) {
          return true;
        }

        // Prevent hot-module files from being included:
        const assetMetaInformation = asset.info || {};

        return !(assetMetaInformation.hotModuleReplacement || assetMetaInformation.development);
      });
      // Prepend the publicPath and append the hash depending on the
      // webpack.output.publicPath and hashOptions
      // E.g. bundle.js -> /bundle.js?hash
      const entryPointPublicPaths = entryPointFiles
        .map(chunkFile => {
          const entryPointPublicPath = publicPath + this.urlencodePath(chunkFile);
          return this.options.hash
            ? this.appendHash(entryPointPublicPath, compilation.hash)
            : entryPointPublicPath;
        });

      entryPointPublicPaths.forEach((entryPointPublicPath) => {
        const extMatch = extensionRegexp.exec(entryPointPublicPath);

        // Skip if the public path is not a .css, .mjs or .js file
        if (!extMatch) {
          return;
        }

        // Skip if this file is already known
        // (e.g. because of common chunk optimizations)
        if (entryPointPublicPathMap[entryPointPublicPath]) {
          return;
        }

        entryPointPublicPathMap[entryPointPublicPath] = true;

        // ext will contain .js or .css, because .mjs recognizes as .js
        const ext = extMatch[1] === 'mjs' ? 'js' : extMatch[1];

        assets[ext].push(entryPointPublicPath);
      });
    }

    return assets;
  }

  /**
   * Once webpack is done with compiling the template into a NodeJS code this function
   * evaluates it to generate the html result
   *
   * The evaluateCompilationResult is only a class function to allow spying during testing.
   * Please change that in a further refactoring
   *
   * @param {string} source
   * @param {string} publicPath
   * @param {string} templateFilename
   * @returns {Promise<string | (() => string | Promise<string>)>}
   */
  evaluateCompilationResult (source, publicPath, templateFilename) {
    if (!source) {
      return Promise.reject(new Error('The child compilation didn\'t provide a result'));
    }

    // The LibraryTemplatePlugin stores the template result in a local variable.
    // By adding it to the end the value gets extracted during evaluation
    if (source.indexOf('HTML_WEBPACK_PLUGIN_RESULT') >= 0) {
      source += ';\nHTML_WEBPACK_PLUGIN_RESULT';
    }

    const templateWithoutLoaders = templateFilename.replace(/^.+!/, '').replace(/\?.+$/, '');
    const vmContext = vm.createContext({
      ...global,
      HTML_WEBPACK_PLUGIN: true,
      require: require,
      htmlWebpackPluginPublicPath: publicPath,
      __filename: templateWithoutLoaders,
      __dirname: path.dirname(templateWithoutLoaders),
      AbortController: global.AbortController,
      AbortSignal: global.AbortSignal,
      Blob: global.Blob,
      Buffer: global.Buffer,
      ByteLengthQueuingStrategy: global.ByteLengthQueuingStrategy,
      BroadcastChannel: global.BroadcastChannel,
      CompressionStream: global.CompressionStream,
      CountQueuingStrategy: global.CountQueuingStrategy,
      Crypto: global.Crypto,
      CryptoKey: global.CryptoKey,
      CustomEvent: global.CustomEvent,
      DecompressionStream: global.DecompressionStream,
      Event: global.Event,
      EventTarget: global.EventTarget,
      File: global.File,
      FormData: global.FormData,
      Headers: global.Headers,
      MessageChannel: global.MessageChannel,
      MessageEvent: global.MessageEvent,
      MessagePort: global.MessagePort,
      PerformanceEntry: global.PerformanceEntry,
      PerformanceMark: global.PerformanceMark,
      PerformanceMeasure: global.PerformanceMeasure,
      PerformanceObserver: global.PerformanceObserver,
      PerformanceObserverEntryList: global.PerformanceObserverEntryList,
      PerformanceResourceTiming: global.PerformanceResourceTiming,
      ReadableByteStreamController: global.ReadableByteStreamController,
      ReadableStream: global.ReadableStream,
      ReadableStreamBYOBReader: global.ReadableStreamBYOBReader,
      ReadableStreamBYOBRequest: global.ReadableStreamBYOBRequest,
      ReadableStreamDefaultController: global.ReadableStreamDefaultController,
      ReadableStreamDefaultReader: global.ReadableStreamDefaultReader,
      Response: global.Response,
      Request: global.Request,
      SubtleCrypto: global.SubtleCrypto,
      DOMException: global.DOMException,
      TextDecoder: global.TextDecoder,
      TextDecoderStream: global.TextDecoderStream,
      TextEncoder: global.TextEncoder,
      TextEncoderStream: global.TextEncoderStream,
      TransformStream: global.TransformStream,
      TransformStreamDefaultController: global.TransformStreamDefaultController,
      URL: global.URL,
      URLSearchParams: global.URLSearchParams,
      WebAssembly: global.WebAssembly,
      WritableStream: global.WritableStream,
      WritableStreamDefaultController: global.WritableStreamDefaultController,
      WritableStreamDefaultWriter: global.WritableStreamDefaultWriter
    });

    const vmScript = new vm.Script(source, { filename: templateWithoutLoaders });

    // Evaluate code and cast to string
    let newSource;

    try {
      newSource = vmScript.runInContext(vmContext);
    } catch (e) {
      return Promise.reject(e);
    }

    if (typeof newSource === 'object' && newSource.__esModule && newSource.default) {
      newSource = newSource.default;
    }

    return typeof newSource === 'string' || typeof newSource === 'function'
      ? Promise.resolve(newSource)
      : Promise.reject(new Error('The loader "' + templateWithoutLoaders + '" didn\'t return html.'));
  }

  /**
   * Add toString methods for easier rendering inside the template
   *
   * @private
   * @param {Array<HtmlTagObject>} assetTagGroup
   * @returns {Array<HtmlTagObject>}
   */
  prepareAssetTagGroupForRendering (assetTagGroup) {
    const xhtml = this.options.xhtml;
    return HtmlTagArray.from(assetTagGroup.map((assetTag) => {
      const copiedAssetTag = Object.assign({}, assetTag);
      copiedAssetTag.toString = function () {
        return htmlTagObjectToString(this, xhtml);
      };
      return copiedAssetTag;
    }));
  }

  /**
   * Generate the template parameters for the template function
   *
   * @private
   * @param {Compilation} compilation
   * @param {AssetsInformationByGroups} assetsInformationByGroups
   * @param {{
       headTags: HtmlTagObject[],
       bodyTags: HtmlTagObject[]
     }} assetTags
   * @returns {Promise<{[key: any]: any}>}
   */
  getTemplateParameters (compilation, assetsInformationByGroups, assetTags) {
    const templateParameters = this.options.templateParameters;

    if (templateParameters === false) {
      return Promise.resolve({});
    }

    if (typeof templateParameters !== 'function' && typeof templateParameters !== 'object') {
      throw new Error('templateParameters has to be either a function or an object');
    }

    const templateParameterFunction = typeof templateParameters === 'function'
      // A custom function can overwrite the entire template parameter preparation
      ? templateParameters
      // If the template parameters is an object merge it with the default values
      : (compilation, assetsInformationByGroups, assetTags, options) => Object.assign({},
        templateParametersGenerator(compilation, assetsInformationByGroups, assetTags, options),
        templateParameters
      );
    const preparedAssetTags = {
      headTags: this.prepareAssetTagGroupForRendering(assetTags.headTags),
      bodyTags: this.prepareAssetTagGroupForRendering(assetTags.bodyTags)
    };
    return Promise
      .resolve()
      .then(() => templateParameterFunction(compilation, assetsInformationByGroups, preparedAssetTags, this.options));
  }

  /**
   * This function renders the actual html by executing the template function
   *
   * @private
   * @param {(templateParameters) => string | Promise<string>} templateFunction
   * @param {AssetsInformationByGroups} assetsInformationByGroups
   * @param {{
       headTags: HtmlTagObject[],
       bodyTags: HtmlTagObject[]
     }} assetTags
   * @param {Compilation} compilation
   * @returns Promise<string>
   */
  executeTemplate (templateFunction, assetsInformationByGroups, assetTags, compilation) {
    // Template processing
    const templateParamsPromise = this.getTemplateParameters(compilation, assetsInformationByGroups, assetTags);

    return templateParamsPromise.then((templateParams) => {
      try {
        // If html is a promise return the promise
        // If html is a string turn it into a promise
        return templateFunction(templateParams);
      } catch (e) {
        compilation.errors.push(new Error('Template execution failed: ' + e));
        return Promise.reject(e);
      }
    });
  }

  /**
   * Html Post processing
   *
   * @private
   * @param {Compiler} compiler The compiler instance
   * @param {any} originalHtml The input html
   * @param {AssetsInformationByGroups} assetsInformationByGroups
   * @param {{headTags: HtmlTagObject[], bodyTags: HtmlTagObject[]}} assetTags The asset tags to inject
   * @returns {Promise<string>}
   */
  postProcessHtml (compiler, originalHtml, assetsInformationByGroups, assetTags) {
    let html = originalHtml;

    if (typeof html !== 'string') {
      return Promise.reject(new Error('Expected html to be a string but got ' + JSON.stringify(html)));
    }

    if (this.options.inject) {
      const htmlRegExp = /(<html[^>]*>)/i;
      const headRegExp = /(<\/head\s*>)/i;
      const bodyRegExp = /(<\/body\s*>)/i;
      const metaViewportRegExp = /<meta[^>]+name=["']viewport["'][^>]*>/i;
      const body = assetTags.bodyTags.map((assetTagObject) => htmlTagObjectToString(assetTagObject, this.options.xhtml));
      const head = assetTags.headTags.filter((item) => {
        if (item.tagName === 'meta' && item.attributes && item.attributes.name === 'viewport' && metaViewportRegExp.test(html)) {
          return false;
        }

        return true;
      }).map((assetTagObject) => htmlTagObjectToString(assetTagObject, this.options.xhtml));

      if (body.length) {
        if (bodyRegExp.test(html)) {
          // Append assets to body element
          html = html.replace(bodyRegExp, match => body.join('') + match);
        } else {
          // Append scripts to the end of the file if no <body> element exists:
          html += body.join('');
        }
      }

      if (head.length) {
        // Create a head tag if none exists
        if (!headRegExp.test(html)) {
          if (!htmlRegExp.test(html)) {
            html = '<head></head>' + html;
          } else {
            html = html.replace(htmlRegExp, match => match + '<head></head>');
          }
        }

        // Append assets to head element
        html = html.replace(headRegExp, match => head.join('') + match);
      }

      // Inject manifest into the opening html tag
      if (assetsInformationByGroups.manifest) {
        html = html.replace(/(<html[^>]*)(>)/i, (match, start, end) => {
          // Append the manifest only if no manifest was specified
          if (/\smanifest\s*=/.test(match)) {
            return match;
          }
          return start + ' manifest="' + assetsInformationByGroups.manifest + '"' + end;
        });
      }
    }

    // TODO avoid this logic and use https://github.com/webpack-contrib/html-minimizer-webpack-plugin under the hood in the next major version
    // Check if webpack is running in production mode
    // @see https://github.com/webpack/webpack/blob/3366421f1784c449f415cda5930a8e445086f688/lib/WebpackOptionsDefaulter.js#L12-L14
    const isProductionLikeMode = compiler.options.mode === 'production' || !compiler.options.mode;
    const needMinify = this.options.minify === true || typeof this.options.minify === 'object' || (this.options.minify === 'auto' && isProductionLikeMode);

    if (!needMinify) {
      return Promise.resolve(html);
    }

    const minifyOptions = typeof this.options.minify === 'object'
      ? this.options.minify
      : {
        // https://www.npmjs.com/package/html-minifier-terser#options-quick-reference
        collapseWhitespace: true,
        keepClosingSlash: true,
        removeComments: true,
        removeRedundantAttributes: true,
        removeScriptTypeAttributes: true,
        removeStyleLinkTypeAttributes: true,
        useShortDoctype: true
      };

    try {
      html = require('html-minifier-terser').minify(html, minifyOptions);
    } catch (e) {
      const isParseError = String(e.message).indexOf('Parse Error') === 0;

      if (isParseError) {
        e.message = 'html-webpack-plugin could not minify the generated output.\n' +
          'In production mode the html minifcation is enabled by default.\n' +
          'If you are not generating a valid html output please disable it manually.\n' +
          'You can do so by adding the following setting to your HtmlWebpackPlugin config:\n|\n|' +
          '    minify: false\n|\n' +
          'See https://github.com/jantimon/html-webpack-plugin#options for details.\n\n' +
          'For parser dedicated bugs please create an issue here:\n' +
          'https://danielruf.github.io/html-minifier-terser/' +
          '\n' + e.message;
      }

      return Promise.reject(e);
    }

    return Promise.resolve(html);
  }

  /**
   * Helper to return a sorted unique array of all asset files out of the asset object
   * @private
   */
  getAssetFiles (assets) {
    const files = _.uniq(Object.keys(assets).filter(assetType => assetType !== 'chunks' && assets[assetType]).reduce((files, assetType) => files.concat(assets[assetType]), []));
    files.sort();
    return files;
  }

  /**
   * Converts a favicon file from disk to a webpack resource and returns the url to the resource
   *
   * @private
   * @param {Compiler} compiler
   * @param {string|false} favicon
   * @param {Compilation} compilation
   * @param {string} publicPath
   * @param {PreviousEmittedAssets} previousEmittedAssets
   * @returns {Promise<string|undefined>}
   */
  generateFavicon (compiler, favicon, compilation, publicPath, previousEmittedAssets) {
    if (!favicon) {
      return Promise.resolve(undefined);
    }

    const filename = path.resolve(compilation.compiler.context, favicon);

    return promisify(compilation.inputFileSystem.readFile)(filename)
      .then((buf) => {
        const source = new compiler.webpack.sources.RawSource(/** @type {string | Buffer} */ (buf), false);
        const name = path.basename(filename);

        compilation.fileDependencies.add(filename);
        compilation.emitAsset(name, source);
        previousEmittedAssets.push({ name, source });

        const faviconPath = publicPath + name;

        if (this.options.hash) {
          return this.appendHash(faviconPath, /** @type {string} */ (compilation.hash));
        }

        return faviconPath;
      })
      .catch(() => Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename)));
  }

  /**
   * Generate all tags script for the given file paths
   *
   * @private
   * @param {Array<string>} jsAssets
   * @returns {Array<HtmlTagObject>}
   */
  generatedScriptTags (jsAssets) {
    // @ts-ignore
    return jsAssets.map(src => {
      const attributes = {};

      if (this.options.scriptLoading === 'defer') {
        attributes.defer = true;
      } else if (this.options.scriptLoading === 'module') {
        attributes.type = 'module';
      } else if (this.options.scriptLoading === 'systemjs-module') {
        attributes.type = 'systemjs-module';
      }

      attributes.src = src;

      return {
        tagName: 'script',
        voidTag: false,
        meta: { plugin: 'html-webpack-plugin' },
        attributes
      };
    });
  }

  /**
   * Generate all style tags for the given file paths
   *
   * @private
   * @param {Array<string>} cssAssets
   * @returns {Array<HtmlTagObject>}
   */
  generateStyleTags (cssAssets) {
    return cssAssets.map(styleAsset => ({
      tagName: 'link',
      voidTag: true,
      meta: { plugin: 'html-webpack-plugin' },
      attributes: {
        href: styleAsset,
        rel: 'stylesheet'
      }
    }));
  }

  /**
   * Generate an optional base tag
   *
   * @param {string | {[attributeName: string]: string}} base
   * @returns {Array<HtmlTagObject>}
   */
  generateBaseTag (base) {
    return [{
      tagName: 'base',
      voidTag: true,
      meta: { plugin: 'html-webpack-plugin' },
      // attributes e.g. { href:"http://example.com/page.html" target:"_blank" }
      attributes: typeof base === 'string' ? {
        href: base
      } : base
    }];
  }

  /**
   * Generate all meta tags for the given meta configuration
   *
   * @private
   * @param {false | {[name: string]:  false | string | {[attributeName: string]: string|boolean}}} metaOptions
   * @returns {Array<HtmlTagObject>}
   */
  generatedMetaTags (metaOptions) {
    if (metaOptions === false) {
      return [];
    }

    // Make tags self-closing in case of xhtml
    // Turn { "viewport" : "width=500, initial-scale=1" } into
    // [{ name:"viewport" content:"width=500, initial-scale=1" }]
    const metaTagAttributeObjects = Object.keys(metaOptions)
      .map((metaName) => {
        const metaTagContent = metaOptions[metaName];
        return (typeof metaTagContent === 'string') ? {
          name: metaName,
          content: metaTagContent
        } : metaTagContent;
      })
      .filter((attribute) => attribute !== false);

    // Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
    // the html-webpack-plugin tag structure
    return metaTagAttributeObjects.map((metaTagAttributes) => {
      if (metaTagAttributes === false) {
        throw new Error('Invalid meta tag');
      }
      return {
        tagName: 'meta',
        voidTag: true,
        meta: { plugin: 'html-webpack-plugin' },
        attributes: metaTagAttributes
      };
    });
  }

  /**
   * Generate a favicon tag for the given file path
   *
   * @private
   * @param {string} favicon
   * @returns {Array<HtmlTagObject>}
   */
  generateFaviconTag (favicon) {
    return [{
      tagName: 'link',
      voidTag: true,
      meta: { plugin: 'html-webpack-plugin' },
      attributes: {
        rel: 'icon',
        href: favicon
      }
    }];
  }

  /**
   * Group assets to head and body tags
   *
   * @param {{
      scripts: Array<HtmlTagObject>;
      styles: Array<HtmlTagObject>;
      meta: Array<HtmlTagObject>;
    }} assetTags
   * @param {"body" | "head"} scriptTarget
   * @returns {{
      headTags: Array<HtmlTagObject>;
      bodyTags: Array<HtmlTagObject>;
    }}
   */
  groupAssetsByElements (assetTags, scriptTarget) {
    /** @type {{ headTags: Array<HtmlTagObject>; bodyTags: Array<HtmlTagObject>; }} */
    const result = {
      headTags: [
        ...assetTags.meta,
        ...assetTags.styles
      ],
      bodyTags: []
    };

    // Add script tags to head or body depending on
    // the htmlPluginOptions
    if (scriptTarget === 'body') {
      result.bodyTags.push(...assetTags.scripts);
    } else {
      // If script loading is blocking add the scripts to the end of the head
      // If script loading is non-blocking add the scripts in front of the css files
      const insertPosition = this.options.scriptLoading === 'blocking' ? result.headTags.length : assetTags.meta.length;

      result.headTags.splice(insertPosition, 0, ...assetTags.scripts);
    }

    return result;
  }

  /**
   * Replace [contenthash] in filename
   *
   * @see https://survivejs.com/webpack/optimizing/adding-hashes-to-filenames/
   *
   * @private
   * @param {Compiler} compiler
   * @param {string} filename
   * @param {string|Buffer} fileContent
   * @param {Compilation} compilation
   * @returns {{ path: string, info: {} }}
   */
  replacePlaceholdersInFilename (compiler, filename, fileContent, compilation) {
    if (/\[\\*([\w:]+)\\*\]/i.test(filename) === false) {
      return { path: filename, info: {} };
    }

    const hash = compiler.webpack.util.createHash(compilation.outputOptions.hashFunction);

    hash.update(fileContent);

    if (compilation.outputOptions.hashSalt) {
      hash.update(compilation.outputOptions.hashSalt);
    }

    const contentHash = hash.digest(compilation.outputOptions.hashDigest).slice(0, compilation.outputOptions.hashDigestLength);

    return compilation.getPathWithInfo(
      filename,
      {
        contentHash,
        chunk: {
          hash: contentHash,
          contentHash
        }
      }
    );
  }

  /**
   * Function to generate HTML file.
   *
   * @private
   * @param {Compiler} compiler
   * @param {Compilation} compilation
   * @param {string} outputName
   * @param {CachedChildCompilation} childCompilerPlugin
   * @param {PreviousEmittedAssets} previousEmittedAssets
   * @param {{ value: string | undefined }} assetJson
   * @param {(err?: Error) => void} callback
   */
  generateHTML (
    compiler,
    compilation,
    outputName,
    childCompilerPlugin,
    previousEmittedAssets,
    assetJson,
    callback
  ) {
    // Get all entry point names for this html file
    const entryNames = Array.from(compilation.entrypoints.keys());
    const filteredEntryNames = this.filterEntryChunks(entryNames, this.options.chunks, this.options.excludeChunks);
    const sortedEntryNames = this.sortEntryChunks(filteredEntryNames, this.options.chunksSortMode, compilation);
    const templateResult = this.options.templateContent
      ? { mainCompilationHash: compilation.hash }
      : childCompilerPlugin.getCompilationEntryResult(this.options.template);

    if ('error' in templateResult) {
      compilation.errors.push(prettyError(templateResult.error, compiler.context).toString());
    }

    // If the child compilation was not executed during a previous main compile run
    // it is a cached result
    const isCompilationCached = templateResult.mainCompilationHash !== compilation.hash;
    /** Generated file paths from the entry point names */
    const assetsInformationByGroups = this.getAssetsInformationByGroups(compilation, outputName, sortedEntryNames);
    // If the template and the assets did not change we don't have to emit the html
    const newAssetJson = JSON.stringify(this.getAssetFiles(assetsInformationByGroups));

    if (isCompilationCached && this.options.cache && assetJson.value === newAssetJson) {
      previousEmittedAssets.forEach(({ name, source, info }) => {
        compilation.emitAsset(name, source, info);
      });
      return callback();
    } else {
      previousEmittedAssets.length = 0;
      assetJson.value = newAssetJson;
    }

    // The html-webpack plugin uses a object representation for the html-tags which will be injected
    // to allow altering them more easily
    // Just before they are converted a third-party-plugin author might change the order and content
    const assetsPromise = this.generateFavicon(compiler, this.options.favicon, compilation, assetsInformationByGroups.publicPath, previousEmittedAssets)
      .then((faviconPath) => {
        assetsInformationByGroups.favicon = faviconPath;
        return getHtmlWebpackPluginHooks(compilation).beforeAssetTagGeneration.promise({
          assets: assetsInformationByGroups,
          outputName,
          plugin: this
        });
      });

    // Turn the js and css paths into grouped HtmlTagObjects
    const assetTagGroupsPromise = assetsPromise
      // And allow third-party-plugin authors to reorder and change the assetTags before they are grouped
      .then(({ assets }) => getHtmlWebpackPluginHooks(compilation).alterAssetTags.promise({
        assetTags: {
          scripts: this.generatedScriptTags(assets.js),
          styles: this.generateStyleTags(assets.css),
          meta: [
            ...(this.options.base !== false ? this.generateBaseTag(this.options.base) : []),
            ...this.generatedMetaTags(this.options.meta),
            ...(assets.favicon ? this.generateFaviconTag(assets.favicon) : [])
          ]
        },
        outputName,
        publicPath: assetsInformationByGroups.publicPath,
        plugin: this
      }))
      .then(({ assetTags }) => {
        // Inject scripts to body unless it set explicitly to head
        const scriptTarget = this.options.inject === 'head' ||
        (this.options.inject !== 'body' && this.options.scriptLoading !== 'blocking') ? 'head' : 'body';
        // Group assets to `head` and `body` tag arrays
        const assetGroups = this.groupAssetsByElements(assetTags, scriptTarget);
        // Allow third-party-plugin authors to reorder and change the assetTags once they are grouped
        return getHtmlWebpackPluginHooks(compilation).alterAssetTagGroups.promise({
          headTags: assetGroups.headTags,
          bodyTags: assetGroups.bodyTags,
          outputName,
          publicPath: assetsInformationByGroups.publicPath,
          plugin: this
        });
      });

    // Turn the compiled template into a nodejs function or into a nodejs string
    const templateEvaluationPromise = Promise.resolve()
      .then(() => {
        if ('error' in templateResult) {
          return this.options.showErrors ? prettyError(templateResult.error, compiler.context).toHtml() : 'ERROR';
        }

        // Allow to use a custom function / string instead
        if (this.options.templateContent !== false) {
          return this.options.templateContent;
        }

        // Once everything is compiled evaluate the html factory and replace it with its content
        if ('compiledEntry' in templateResult) {
          const compiledEntry = templateResult.compiledEntry;
          const assets = compiledEntry.assets;

          // Store assets from child compiler to reemit them later
          for (const name in assets) {
            previousEmittedAssets.push({ name, source: assets[name].source, info: assets[name].info });
          }

          return this.evaluateCompilationResult(compiledEntry.content, assetsInformationByGroups.publicPath, this.options.template);
        }

        return Promise.reject(new Error('Child compilation contained no compiledEntry'));
      });
    const templateExectutionPromise = Promise.all([assetsPromise, assetTagGroupsPromise, templateEvaluationPromise])
      // Execute the template
      .then(([assetsHookResult, assetTags, compilationResult]) => typeof compilationResult !== 'function'
        ? compilationResult
        : this.executeTemplate(compilationResult, assetsHookResult.assets, { headTags: assetTags.headTags, bodyTags: assetTags.bodyTags }, compilation));

    const injectedHtmlPromise = Promise.all([assetTagGroupsPromise, templateExectutionPromise])
      // Allow plugins to change the html before assets are injected
      .then(([assetTags, html]) => {
        const pluginArgs = { html, headTags: assetTags.headTags, bodyTags: assetTags.bodyTags, plugin: this, outputName };
        return getHtmlWebpackPluginHooks(compilation).afterTemplateExecution.promise(pluginArgs);
      })
      .then(({ html, headTags, bodyTags }) => {
        return this.postProcessHtml(compiler, html, assetsInformationByGroups, { headTags, bodyTags });
      });

    const emitHtmlPromise = injectedHtmlPromise
      // Allow plugins to change the html after assets are injected
      .then((html) => {
        const pluginArgs = { html, plugin: this, outputName };
        return getHtmlWebpackPluginHooks(compilation).beforeEmit.promise(pluginArgs)
          .then(result => result.html);
      })
      .catch(err => {
        // In case anything went wrong the promise is resolved
        // with the error message and an error is logged
        compilation.errors.push(prettyError(err, compiler.context).toString());
        return this.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
      })
      .then(html => {
        const filename = outputName.replace(/\[templatehash([^\]]*)\]/g, require('util').deprecate(
          (match, options) => `[contenthash${options}]`,
          '[templatehash] is now [contenthash]')
        );
        const replacedFilename = this.replacePlaceholdersInFilename(compiler, filename, html, compilation);
        const source = new compiler.webpack.sources.RawSource(html, false);

        // Add the evaluated html code to the webpack assets
        compilation.emitAsset(replacedFilename.path, source, replacedFilename.info);
        previousEmittedAssets.push({ name: replacedFilename.path, source });

        return replacedFilename.path;
      })
      .then((finalOutputName) => getHtmlWebpackPluginHooks(compilation).afterEmit.promise({
        outputName: finalOutputName,
        plugin: this
      }).catch(err => {
        /** @type {Logger} */
        (this.logger).error(err);
        return null;
      }).then(() => null));

    // Once all files are added to the webpack compilation
    // let the webpack compiler continue
    emitHtmlPromise.then(() => {
      callback();
    });
  }
}

/**
 * The default for options.templateParameter
 * Generate the template parameters
 *
 * Generate the template parameters for the template function
 * @param {Compilation} compilation
 * @param {AssetsInformationByGroups} assets
 * @param {{
     headTags: HtmlTagObject[],
     bodyTags: HtmlTagObject[]
   }} assetTags
 * @param {ProcessedHtmlWebpackOptions} options
 * @returns {TemplateParameter}
 */
function templateParametersGenerator (compilation, assets, assetTags, options) {
  return {
    compilation: compilation,
    webpackConfig: compilation.options,
    htmlWebpackPlugin: {
      tags: assetTags,
      files: assets,
      options: options
    }
  };
}

// Statics:
/**
 * The major version number of this plugin
 */
HtmlWebpackPlugin.version = 5;

/**
 * A static helper to get the hooks for this plugin
 *
 * Usage: HtmlWebpackPlugin.getHooks(compilation).HOOK_NAME.tapAsync('YourPluginName', () => { ... });
 */
HtmlWebpackPlugin.getHooks = getHtmlWebpackPluginHooks;
HtmlWebpackPlugin.createHtmlTagObject = createHtmlTagObject;

module.exports = HtmlWebpackPlugin;


code:
// @ts-check
/**
 * @file
 * Helper plugin manages the cached state of the child compilation
 *
 * To optimize performance the child compilation is running asyncronously.
 * Therefore it needs to be started in the compiler.make phase and ends after
 * the compilation.afterCompile phase.
 *
 * To prevent bugs from blocked hooks there is no promise or event based api
 * for this plugin.
 *
 * Example usage:
 *
 * ```js
    const childCompilerPlugin = new PersistentChildCompilerPlugin();
    childCompilerPlugin.addEntry('./src/index.js');
    compiler.hooks.afterCompile.tapAsync('MyPlugin', (compilation, callback) => {
      console.log(childCompilerPlugin.getCompilationResult()['./src/index.js']));
      return true;
    });
 * ```
 */
'use strict';

// Import types
/** @typedef {import("webpack").Compiler} Compiler */
/** @typedef {import("webpack").Compilation} Compilation */
/** @typedef {import("webpack/lib/FileSystemInfo").Snapshot} Snapshot */
/** @typedef {import("./child-compiler").ChildCompilationTemplateResult} ChildCompilationTemplateResult */
/** @typedef {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}} FileDependencies */
/** @typedef {{
  dependencies: FileDependencies,
  compiledEntries: {[entryName: string]: ChildCompilationTemplateResult}
} | {
  dependencies: FileDependencies,
  error: Error
}} ChildCompilationResult */

const { HtmlWebpackChildCompiler } = require('./child-compiler');

/**
 * This plugin is a singleton for performance reasons.
 * To keep track if a plugin does already exist for the compiler they are cached
 * in this map
 * @type {WeakMap<Compiler, PersistentChildCompilerSingletonPlugin>}}
 */
const compilerMap = new WeakMap();

class CachedChildCompilation {
  /**
   * @param {Compiler} compiler
   */
  constructor (compiler) {
    /**
     * @private
     * @type {Compiler}
     */
    this.compiler = compiler;
    // Create a singleton instance for the compiler
    // if there is none
    if (compilerMap.has(compiler)) {
      return;
    }
    const persistentChildCompilerSingletonPlugin = new PersistentChildCompilerSingletonPlugin();
    compilerMap.set(compiler, persistentChildCompilerSingletonPlugin);
    persistentChildCompilerSingletonPlugin.apply(compiler);
  }

  /**
   * apply is called by the webpack main compiler during the start phase
   * @param {string} entry
   */
  addEntry (entry) {
    const persistentChildCompilerSingletonPlugin = compilerMap.get(this.compiler);
    if (!persistentChildCompilerSingletonPlugin) {
      throw new Error(
        'PersistentChildCompilerSingletonPlugin instance not found.'
      );
    }
    persistentChildCompilerSingletonPlugin.addEntry(entry);
  }

  getCompilationResult () {
    const persistentChildCompilerSingletonPlugin = compilerMap.get(this.compiler);
    if (!persistentChildCompilerSingletonPlugin) {
      throw new Error(
        'PersistentChildCompilerSingletonPlugin instance not found.'
      );
    }
    return persistentChildCompilerSingletonPlugin.getLatestResult();
  }

  /**
   * Returns the result for the given entry
   * @param {string} entry
   * @returns {
      | { mainCompilationHash: string, error: Error }
      | { mainCompilationHash: string, compiledEntry: ChildCompilationTemplateResult }
    }
   */
  getCompilationEntryResult (entry) {
    const latestResult = this.getCompilationResult();
    const compilationResult = latestResult.compilationResult;
    return 'error' in compilationResult ? {
      mainCompilationHash: latestResult.mainCompilationHash,
      error: compilationResult.error
    } : {
      mainCompilationHash: latestResult.mainCompilationHash,
      compiledEntry: compilationResult.compiledEntries[entry]
    };
  }
}

class PersistentChildCompilerSingletonPlugin {
  /**
   *
   * @param {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}} fileDependencies
   * @param {Compilation} mainCompilation
   * @param {number} startTime
   */
  static createSnapshot (fileDependencies, mainCompilation, startTime) {
    return new Promise((resolve, reject) => {
      mainCompilation.fileSystemInfo.createSnapshot(
        startTime,
        fileDependencies.fileDependencies,
        fileDependencies.contextDependencies,
        fileDependencies.missingDependencies,
        // @ts-ignore
        null,
        (err, snapshot) => {
          if (err) {
            return reject(err);
          }
          resolve(snapshot);
        }
      );
    });
  }

  /**
   * Returns true if the files inside this snapshot
   * have not been changed
   *
   * @param {Snapshot} snapshot
   * @param {Compilation} mainCompilation
   * @returns {Promise<boolean | undefined>}
   */
  static isSnapshotValid (snapshot, mainCompilation) {
    return new Promise((resolve, reject) => {
      mainCompilation.fileSystemInfo.checkSnapshotValid(
        snapshot,
        (err, isValid) => {
          if (err) {
            reject(err);
          }
          resolve(isValid);
        }
      );
    });
  }

  static watchFiles (mainCompilation, fileDependencies) {
    Object.keys(fileDependencies).forEach((depencyTypes) => {
      fileDependencies[depencyTypes].forEach(fileDependency => {
        mainCompilation[depencyTypes].add(fileDependency);
      });
    });
  }

  constructor () {
    /**
     * @private
     * @type {
      | {
        isCompiling: false,
        isVerifyingCache: false,
        entries: string[],
        compiledEntries: string[],
        mainCompilationHash: string,
        compilationResult: ChildCompilationResult
      }
    | Readonly<{
      isCompiling: false,
      isVerifyingCache: true,
      entries: string[],
      previousEntries: string[],
      previousResult: ChildCompilationResult
    }>
    | Readonly <{
      isVerifyingCache: false,
      isCompiling: true,
      entries: string[],
    }>
  } the internal compilation state */
    this.compilationState = {
      isCompiling: false,
      isVerifyingCache: false,
      entries: [],
      compiledEntries: [],
      mainCompilationHash: 'initial',
      compilationResult: {
        dependencies: {
          fileDependencies: [],
          contextDependencies: [],
          missingDependencies: []
        },
        compiledEntries: {}
      }
    };
  }

  /**
   * apply is called by the webpack main compiler during the start phase
   * @param {Compiler} compiler
   */
  apply (compiler) {
    /** @type Promise<ChildCompilationResult> */
    let childCompilationResultPromise = Promise.resolve({
      dependencies: {
        fileDependencies: [],
        contextDependencies: [],
        missingDependencies: []
      },
      compiledEntries: {}
    });
    /**
     * The main compilation hash which will only be updated
     * if the childCompiler changes
     */
    /** @type {string} */
    let mainCompilationHashOfLastChildRecompile = '';
    /** @type {Snapshot | undefined} */
    let previousFileSystemSnapshot;
    let compilationStartTime = new Date().getTime();

    compiler.hooks.make.tapAsync(
      'PersistentChildCompilerSingletonPlugin',
      (mainCompilation, callback) => {
        if (this.compilationState.isCompiling || this.compilationState.isVerifyingCache) {
          return callback(new Error('Child compilation has already started'));
        }

        // Update the time to the current compile start time
        compilationStartTime = new Date().getTime();

        // The compilation starts - adding new templates is now not possible anymore
        this.compilationState = {
          isCompiling: false,
          isVerifyingCache: true,
          previousEntries: this.compilationState.compiledEntries,
          previousResult: this.compilationState.compilationResult,
          entries: this.compilationState.entries
        };

        // Validate cache:
        const isCacheValidPromise = this.isCacheValid(previousFileSystemSnapshot, mainCompilation);

        let cachedResult = childCompilationResultPromise;
        childCompilationResultPromise = isCacheValidPromise.then((isCacheValid) => {
          // Reuse cache
          if (isCacheValid) {
            return cachedResult;
          }
          // Start the compilation
          const compiledEntriesPromise = this.compileEntries(
            mainCompilation,
            this.compilationState.entries
          );
          // Update snapshot as soon as we know the filedependencies
          // this might possibly cause bugs if files were changed inbetween
          // compilation start and snapshot creation
          compiledEntriesPromise.then((childCompilationResult) => {
            return PersistentChildCompilerSingletonPlugin.createSnapshot(childCompilationResult.dependencies, mainCompilation, compilationStartTime);
          }).then((snapshot) => {
            previousFileSystemSnapshot = snapshot;
          });
          return compiledEntriesPromise;
        });

        // Add files to compilation which needs to be watched:
        mainCompilation.hooks.optimizeTree.tapAsync(
          'PersistentChildCompilerSingletonPlugin',
          (chunks, modules, callback) => {
            const handleCompilationDonePromise = childCompilationResultPromise.then(
              childCompilationResult => {
                this.watchFiles(
                  mainCompilation,
                  childCompilationResult.dependencies
                );
              });
            // @ts-ignore
            handleCompilationDonePromise.then(() => callback(null, chunks, modules), callback);
          }
        );

        // Store the final compilation once the main compilation hash is known
        mainCompilation.hooks.additionalAssets.tapAsync(
          'PersistentChildCompilerSingletonPlugin',
          (callback) => {
            const didRecompilePromise = Promise.all([childCompilationResultPromise, cachedResult]).then(
              ([childCompilationResult, cachedResult]) => {
                // Update if childCompilation changed
                return (cachedResult !== childCompilationResult);
              }
            );

            const handleCompilationDonePromise = Promise.all([childCompilationResultPromise, didRecompilePromise]).then(
              ([childCompilationResult, didRecompile]) => {
                // Update hash and snapshot if childCompilation changed
                if (didRecompile) {
                  mainCompilationHashOfLastChildRecompile = /** @type {string} */ (mainCompilation.hash);
                }
                this.compilationState = {
                  isCompiling: false,
                  isVerifyingCache: false,
                  entries: this.compilationState.entries,
                  compiledEntries: this.compilationState.entries,
                  compilationResult: childCompilationResult,
                  mainCompilationHash: mainCompilationHashOfLastChildRecompile
                };
              });
            handleCompilationDonePromise.then(() => callback(null), callback);
          }
        );

        // Continue compilation:
        callback(null);
      }
    );
  }

  /**
   * Add a new entry to the next compile run
   * @param {string} entry
   */
  addEntry (entry) {
    if (this.compilationState.isCompiling || this.compilationState.isVerifyingCache) {
      throw new Error(
        'The child compiler has already started to compile. ' +
        "Please add entries before the main compiler 'make' phase has started or " +
        'after the compilation is done.'
      );
    }
    if (this.compilationState.entries.indexOf(entry) === -1) {
      this.compilationState.entries = [...this.compilationState.entries, entry];
    }
  }

  getLatestResult () {
    if (this.compilationState.isCompiling || this.compilationState.isVerifyingCache) {
      throw new Error(
        'The child compiler is not done compiling. ' +
        "Please access the result after the compiler 'make' phase has started or " +
        'after the compilation is done.'
      );
    }
    return {
      mainCompilationHash: this.compilationState.mainCompilationHash,
      compilationResult: this.compilationState.compilationResult
    };
  }

  /**
   * Verify that the cache is still valid
   * @private
   * @param {Snapshot | undefined} snapshot
   * @param {Compilation} mainCompilation
   * @returns {Promise<boolean | undefined>}
   */
  isCacheValid (snapshot, mainCompilation) {
    if (!this.compilationState.isVerifyingCache) {
      return Promise.reject(new Error('Cache validation can only be done right before the compilation starts'));
    }
    // If there are no entries we don't need a new child compilation
    if (this.compilationState.entries.length === 0) {
      return Promise.resolve(true);
    }
    // If there are new entries the cache is invalid
    if (this.compilationState.entries !== this.compilationState.previousEntries) {
      return Promise.resolve(false);
    }
    // Mark the cache as invalid if there is no snapshot
    if (!snapshot) {
      return Promise.resolve(false);
    }

    return PersistentChildCompilerSingletonPlugin.isSnapshotValid(snapshot, mainCompilation);
  }

  /**
   * Start to compile all templates
   *
   * @private
   * @param {Compilation} mainCompilation
   * @param {string[]} entries
   * @returns {Promise<ChildCompilationResult>}
   */
  compileEntries (mainCompilation, entries) {
    const compiler = new HtmlWebpackChildCompiler(entries);
    return compiler.compileTemplates(mainCompilation).then((result) => {
      return {
      // The compiled sources to render the content
        compiledEntries: result,
        // The file dependencies to find out if a
        // recompilation is required
        dependencies: compiler.fileDependencies,
        // The main compilation hash can be used to find out
        // if this compilation was done during the current compilation
        mainCompilationHash: mainCompilation.hash
      };
    }, error => ({
      // The compiled sources to render the content
      error,
      // The file dependencies to find out if a
      // recompilation is required
      dependencies: compiler.fileDependencies,
      // The main compilation hash can be used to find out
      // if this compilation was done during the current compilation
      mainCompilationHash: mainCompilation.hash
    }));
  }

  /**
   * @private
   * @param {Compilation} mainCompilation
   * @param {FileDependencies} files
   */
  watchFiles (mainCompilation, files) {
    PersistentChildCompilerSingletonPlugin.watchFiles(mainCompilation, files);
  }
}

module.exports = {
  CachedChildCompilation
};

license: mit


code:
// @ts-check
'use strict';

/**
 * @file
 * This file uses webpack to compile a template with a child compiler.
 *
 * [TEMPLATE] -> [JAVASCRIPT]
 *
 */

/** @typedef {import("webpack").Chunk} Chunk */
/** @typedef {import("webpack").sources.Source} Source */
/** @typedef {{hash: string, entry: Chunk, content: string, assets: {[name: string]: { source: Source, info: import("webpack").AssetInfo }}}} ChildCompilationTemplateResult */

/**
 * The HtmlWebpackChildCompiler is a helper to allow reusing one childCompiler
 * for multiple HtmlWebpackPlugin instances to improve the compilation performance.
 */
class HtmlWebpackChildCompiler {
  /**
   *
   * @param {string[]} templates
   */
  constructor (templates) {
    /**
     * @type {string[]} templateIds
     * The template array will allow us to keep track which input generated which output
     */
    this.templates = templates;
    /** @type {Promise<{[templatePath: string]: ChildCompilationTemplateResult}>} */
    this.compilationPromise; // eslint-disable-line
    /** @type {number | undefined} */
    this.compilationStartedTimestamp; // eslint-disable-line
    /** @type {number | undefined} */
    this.compilationEndedTimestamp; // eslint-disable-line
    /**
     * All file dependencies of the child compiler
     * @type {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}}
     */
    this.fileDependencies = { fileDependencies: [], contextDependencies: [], missingDependencies: [] };
  }

  /**
   * Returns true if the childCompiler is currently compiling
   *
   * @returns {boolean}
   */
  isCompiling () {
    return !this.didCompile() && this.compilationStartedTimestamp !== undefined;
  }

  /**
   * Returns true if the childCompiler is done compiling
   *
   * @returns {boolean}
   */
  didCompile () {
    return this.compilationEndedTimestamp !== undefined;
  }

  /**
   * This function will start the template compilation
   * once it is started no more templates can be added
   *
   * @param {import('webpack').Compilation} mainCompilation
   * @returns {Promise<{[templatePath: string]: ChildCompilationTemplateResult}>}
   */
  compileTemplates (mainCompilation) {
    const webpack = mainCompilation.compiler.webpack;
    const Compilation = webpack.Compilation;

    const NodeTemplatePlugin = webpack.node.NodeTemplatePlugin;
    const NodeTargetPlugin = webpack.node.NodeTargetPlugin;
    const LoaderTargetPlugin = webpack.LoaderTargetPlugin;
    const EntryPlugin = webpack.EntryPlugin;

    // To prevent multiple compilations for the same template
    // the compilation is cached in a promise.
    // If it already exists return
    if (this.compilationPromise) {
      return this.compilationPromise;
    }

    const outputOptions = {
      filename: '__child-[name]',
      publicPath: '',
      library: {
        type: 'var',
        name: 'HTML_WEBPACK_PLUGIN_RESULT'
      },
      scriptType: /** @type {'text/javascript'} */('text/javascript'),
      iife: true
    };
    const compilerName = 'HtmlWebpackCompiler';
    // Create an additional child compiler which takes the template
    // and turns it into an Node.JS html factory.
    // This allows us to use loaders during the compilation
    const childCompiler = mainCompilation.createChildCompiler(compilerName, outputOptions, [
      // Compile the template to nodejs javascript
      new NodeTargetPlugin(),
      new NodeTemplatePlugin(),
      new LoaderTargetPlugin('node'),
      new webpack.library.EnableLibraryPlugin('var')
    ]);
    // The file path context which webpack uses to resolve all relative files to
    childCompiler.context = mainCompilation.compiler.context;

    // Generate output file names
    const temporaryTemplateNames = this.templates.map((template, index) => `__child-HtmlWebpackPlugin_${index}-${template}`);

    // Add all templates
    this.templates.forEach((template, index) => {
      new EntryPlugin(childCompiler.context, 'data:text/javascript,__webpack_public_path__ = __webpack_base_uri__ = htmlWebpackPluginPublicPath;', `HtmlWebpackPlugin_${index}-${template}`).apply(childCompiler);
      new EntryPlugin(childCompiler.context, template, `HtmlWebpackPlugin_${index}-${template}`).apply(childCompiler);
    });

    // The templates are compiled and executed by NodeJS - similar to server side rendering
    // Unfortunately this causes issues as some loaders require an absolute URL to support ES Modules
    // The following config enables relative URL support for the child compiler
    childCompiler.options.module = { ...childCompiler.options.module };
    childCompiler.options.module.parser = { ...childCompiler.options.module.parser };
    childCompiler.options.module.parser.javascript = {
      ...childCompiler.options.module.parser.javascript,
      url: 'relative'
    };

    this.compilationStartedTimestamp = new Date().getTime();
    /** @type {Promise<{[templatePath: string]: ChildCompilationTemplateResult}>} */
    this.compilationPromise = new Promise((resolve, reject) => {
      /** @type {Source[]} */
      const extractedAssets = [];

      childCompiler.hooks.thisCompilation.tap('HtmlWebpackPlugin', (compilation) => {
        compilation.hooks.processAssets.tap(
          {
            name: 'HtmlWebpackPlugin',
            stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS
          },
          (assets) => {
            temporaryTemplateNames.forEach((temporaryTemplateName) => {
              if (assets[temporaryTemplateName]) {
                extractedAssets.push(assets[temporaryTemplateName]);

                compilation.deleteAsset(temporaryTemplateName);
              }
            });
          }
        );
      });

      childCompiler.runAsChild((err, entries, childCompilation) => {
        // Extract templates
        // TODO fine a better way to store entries and results, to avoid duplicate chunks and assets
        const compiledTemplates = entries
          ? extractedAssets.map((asset) => asset.source())
          : [];

        // Extract file dependencies
        if (entries && childCompilation) {
          this.fileDependencies = { fileDependencies: Array.from(childCompilation.fileDependencies), contextDependencies: Array.from(childCompilation.contextDependencies), missingDependencies: Array.from(childCompilation.missingDependencies) };
        }

        // Reject the promise if the childCompilation contains error
        if (childCompilation && childCompilation.errors && childCompilation.errors.length) {
          const errorDetails = childCompilation.errors.map(error => {
            let message = error.message;
            if (error.stack) {
              message += '\n' + error.stack;
            }
            return message;
          }).join('\n');

          reject(new Error('Child compilation failed:\n' + errorDetails));

          return;
        }

        // Reject if the error object contains errors
        if (err) {
          reject(err);
          return;
        }

        if (!childCompilation || !entries) {
          reject(new Error('Empty child compilation'));
          return;
        }

        /**
         * @type {{[templatePath: string]: ChildCompilationTemplateResult}}
         */
        const result = {};

        /** @type {{[name: string]: { source: Source, info: import("webpack").AssetInfo }}} */
        const assets = {};

        for (const asset of childCompilation.getAssets()) {
          assets[asset.name] = { source: asset.source, info: asset.info };
        }

        compiledTemplates.forEach((templateSource, entryIndex) => {
          // The compiledTemplates are generated from the entries added in
          // the addTemplate function.
          // Therefore, the array index of this.templates should be the as entryIndex.
          result[this.templates[entryIndex]] = {
            // TODO, can we have Buffer here?
            content: /** @type {string} */ (templateSource),
            hash: childCompilation.hash || 'XXXX',
            entry: entries[entryIndex],
            assets
          };
        });

        this.compilationEndedTimestamp = new Date().getTime();

        resolve(result);
      });
    });

    return this.compilationPromise;
  }
}

module.exports = {
  HtmlWebpackChildCompiler
};


code:
// @ts-check
'use strict';

/** @typedef {import("webpack").Compilation} Compilation */

/**
 * @type {{[sortmode: string] : (entryPointNames: Array<string>, compilation: Compilation, htmlWebpackPluginOptions: any) => Array<string> }}
 * This file contains different sort methods for the entry chunks names
 */
module.exports = {};

/**
 * Performs identity mapping (no-sort).
 * @param  {Array<string>} chunks the chunks to sort
 * @return {Array<string>} The sorted chunks
 */
module.exports.none = chunks => chunks;

/**
 * Sort manually by the chunks
 * @param {string[]} entryPointNames the chunks to sort
 * @param {Compilation} compilation the webpack compilation
 * @param {any} htmlWebpackPluginOptions the plugin options
 * @return {string[]} The sorted chunks
 */
module.exports.manual = (entryPointNames, compilation, htmlWebpackPluginOptions) => {
  const chunks = htmlWebpackPluginOptions.chunks;
  if (!Array.isArray(chunks)) {
    return entryPointNames;
  }
  // Remove none existing entries from
  // htmlWebpackPluginOptions.chunks
  return chunks.filter((entryPointName) => {
    return compilation.entrypoints.has(entryPointName);
  });
};

/**
 * Defines the default sorter.
 */
module.exports.auto = module.exports.none;


code:
// @ts-nocheck
'use strict';

let prettyError;

function getPrettyError () {
  if (!prettyError) {
    // lazily require to improve startup time since pretty-error is rather heavy package
    const PrettyError = require('pretty-error');
    prettyError = new PrettyError();
    prettyError.withoutColors();
    prettyError.skipPackage('html-plugin-evaluation');
    prettyError.skipNodeFiles();
    prettyError.skip(function (traceLine) {
      return traceLine.path === 'html-plugin-evaluation';
    });
  }
  return prettyError;
}

module.exports = function (err, context) {
  return {
    toHtml: function () {
      return 'Html Webpack Plugin:\n<pre>\n' + this.toString() + '</pre>';
    },
    toJsonHtml: function () {
      return JSON.stringify(this.toHtml());
    },
    toString: function () {
      try {
        return getPrettyError().render(err).replace(/webpack:\/\/\/\./g, context);
      } catch (e) {
        // This can sometimes fail. We don't know why, but returning the
        // original error is better than returning the error thrown by
        // pretty-error.
        return err;
      }
    }
  };
};


code:
// @ts-check
'use strict';
/**
 * This file provides access to all public htmlWebpackPlugin hooks
 */

/** @typedef {import("webpack").Compilation} WebpackCompilation */
/** @typedef {import("../index.js")} HtmlWebpackPlugin */
/** @typedef {import("../typings").Hooks} HtmlWebpackPluginHooks */

const AsyncSeriesWaterfallHook = require('tapable').AsyncSeriesWaterfallHook;

// The following is the API definition for all available hooks
// For the TypeScript definition, see the Hooks type in typings.d.ts
/**
  beforeAssetTagGeneration:
    AsyncSeriesWaterfallHook<{
      assets: {
        publicPath: string,
        js: Array<string>,
        css: Array<string>,
        favicon?: string | undefined,
        manifest?: string | undefined
      },
      outputName: string,
      plugin: HtmlWebpackPlugin
    }>,
  alterAssetTags:
    AsyncSeriesWaterfallHook<{
      assetTags: {
        scripts: Array<HtmlTagObject>,
        styles: Array<HtmlTagObject>,
        meta: Array<HtmlTagObject>,
      },
      publicPath: string,
      outputName: string,
      plugin: HtmlWebpackPlugin
    }>,
  alterAssetTagGroups:
    AsyncSeriesWaterfallHook<{
      headTags: Array<HtmlTagObject | HtmlTagObject>,
      bodyTags: Array<HtmlTagObject | HtmlTagObject>,
      publicPath: string,
      outputName: string,
      plugin: HtmlWebpackPlugin
    }>,
  afterTemplateExecution:
    AsyncSeriesWaterfallHook<{
      html: string,
      headTags: Array<HtmlTagObject | HtmlTagObject>,
      bodyTags: Array<HtmlTagObject | HtmlTagObject>,
      outputName: string,
      plugin: HtmlWebpackPlugin,
    }>,
  beforeEmit:
    AsyncSeriesWaterfallHook<{
      html: string,
      outputName: string,
      plugin: HtmlWebpackPlugin,
    }>,
  afterEmit:
    AsyncSeriesWaterfallHook<{
      outputName: string,
      plugin: HtmlWebpackPlugin
    }>
*/

/**
 * @type {WeakMap<WebpackCompilation, HtmlWebpackPluginHooks>}}
 */
const htmlWebpackPluginHooksMap = new WeakMap();

/**
 * Returns all public hooks of the html webpack plugin for the given compilation
 *
 * @param {WebpackCompilation} compilation
 * @returns {HtmlWebpackPluginHooks}
 */
function getHtmlWebpackPluginHooks (compilation) {
  let hooks = htmlWebpackPluginHooksMap.get(compilation);
  // Setup the hooks only once
  if (hooks === undefined) {
    hooks = createHtmlWebpackPluginHooks();
    htmlWebpackPluginHooksMap.set(compilation, hooks);
  }
  return hooks;
}

/**
 * Add hooks to the webpack compilation object to allow foreign plugins to
 * extend the HtmlWebpackPlugin
 *
 * @returns {HtmlWebpackPluginHooks}
 */
function createHtmlWebpackPluginHooks () {
  return {
    beforeAssetTagGeneration: new AsyncSeriesWaterfallHook(['pluginArgs']),
    alterAssetTags: new AsyncSeriesWaterfallHook(['pluginArgs']),
    alterAssetTagGroups: new AsyncSeriesWaterfallHook(['pluginArgs']),
    afterTemplateExecution: new AsyncSeriesWaterfallHook(['pluginArgs']),
    beforeEmit: new AsyncSeriesWaterfallHook(['pluginArgs']),
    afterEmit: new AsyncSeriesWaterfallHook(['pluginArgs'])
  };
}

module.exports = {
  getHtmlWebpackPluginHooks
};


code:
// @ts-check
/** @typedef {import("../typings").HtmlTagObject} HtmlTagObject */
/**
 * @file
 * This file provides to helper to create html as a object representation as
 * those objects are easier to modify than pure string representations
 *
 * Usage:
 * ```
 * const element = createHtmlTagObject('h1', {class: 'demo'}, 'Hello World');
 * const html = htmlTagObjectToString(element);
 * console.log(html) // -> <h1 class="demo">Hello World</h1>
 * ```
 */

/**
 * All html tag elements which must not contain innerHTML
 * @see https://www.w3.org/TR/html5/syntax.html#void-elements
 */
const voidTags = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];

/**
 * Turn a tag definition into a html string
 * @param {HtmlTagObject} tagDefinition
 *  A tag element according to the htmlWebpackPlugin object notation
 *
 * @param xhtml {boolean}
 *   Whether the generated html should add closing slashes to be xhtml compliant
 */
function htmlTagObjectToString (tagDefinition, xhtml) {
  const attributes = Object.keys(tagDefinition.attributes || {})
    .filter(function (attributeName) {
      return tagDefinition.attributes[attributeName] === '' || tagDefinition.attributes[attributeName];
    })
    .map(function (attributeName) {
      if (tagDefinition.attributes[attributeName] === true) {
        return xhtml ? attributeName + '="' + attributeName + '"' : attributeName;
      }
      return attributeName + '="' + tagDefinition.attributes[attributeName] + '"';
    });
  return '<' + [tagDefinition.tagName].concat(attributes).join(' ') + (tagDefinition.voidTag && xhtml ? '/' : '') + '>' +
    (tagDefinition.innerHTML || '') +
    (tagDefinition.voidTag ? '' : '</' + tagDefinition.tagName + '>');
}

/**
 * Static helper to create a tag object to be get injected into the dom
 *
 * @param {string} tagName
 * the name of the tag e.g. 'div'
 *
 * @param {{[attributeName: string]: string|boolean|null|undefined}} [attributes]
 * tag attributes e.g. `{ 'class': 'example', disabled: true }`
 *
 * @param {string} [innerHTML]
 *
 * @param {{[attributeName: string]: string|boolean|null|undefined}} [meta]
 * meta information about the tag e.g. `{ 'plugin': 'html-webpack-plugin' }`
 *
 * @returns {HtmlTagObject}
 */
function createHtmlTagObject (tagName, attributes, innerHTML, meta) {
  return {
    tagName: tagName,
    voidTag: voidTags.indexOf(tagName) !== -1,
    attributes: attributes || {},
    meta: meta || {},
    innerHTML: innerHTML
  };
}

/**
 * The `HtmlTagArray Array with a custom `.toString()` method.
 *
 * This allows the following:
 * ```
 *   const tags = HtmlTagArray.from([tag1, tag2]);
 *   const scriptTags = tags.filter((tag) => tag.tagName === 'script');
 *   const html = scriptTags.toString();
 * ```
 *
 * Or inside a string literal:
 * ```
 *   const tags = HtmlTagArray.from([tag1, tag2]);
 *   const html = `<html><body>${tags.filter((tag) => tag.tagName === 'script')}</body></html>`;
 * ```
 *
 */
class HtmlTagArray extends Array {
  toString () {
    return this.join('');
  }
}

module.exports = {
  HtmlTagArray: HtmlTagArray,
  createHtmlTagObject: createHtmlTagObject,
  htmlTagObjectToString: htmlTagObjectToString
};


code:
/* This loader renders the template with underscore if no other loader was found */
// @ts-nocheck
'use strict';
const _ = require('lodash');

module.exports = function (source) {
  // Get templating options
  const options = this.getOptions();
  const force = options.force || false;

  const allLoadersButThisOne = this.loaders.filter((loader) => loader.normal !== module.exports);

  // This loader shouldn't kick in if there is any other loader (unless it's explicitly enforced)
  if (allLoadersButThisOne.length > 0 && !force) {
    return source;
  }

  // Allow only one html-webpack-plugin loader to allow loader options in the webpack config
  const htmlWebpackPluginLoaders = this.loaders.filter((loader) => loader.normal === module.exports);
  const lastHtmlWebpackPluginLoader = htmlWebpackPluginLoaders[htmlWebpackPluginLoaders.length - 1];
  if (this.loaders[this.loaderIndex] !== lastHtmlWebpackPluginLoader) {
    return source;
  }

  // Skip .js files (unless it's explicitly enforced)
  if (/\.(c|m)?js$/.test(this.resourcePath) && !force) {
    return source;
  }

  // The following part renders the template with lodash as a minimalistic loader
  //
  const template = _.template(source, { interpolate: /<%=([\s\S]+?)%>/g, variable: 'data', ...options });
  // Use `eval("require")("lodash")` to enforce using the native nodejs require
  // during template execution
  return 'var _ = eval("require")(' + JSON.stringify(require.resolve('lodash')) + ');' +
    'module.exports = function (templateParams) { with(templateParams) {' +
      // Execute the lodash template
      'return (' + template.source + ')();' +
    '}}';
};


html-webpack-plugin

license: MIT

description: Simplifies creation of HTML files to serve your webpack bundles

author: Jan Nicklas <j.nicklas@me.com> (https://github.com/jantimon)

pretest: semistandard

posttest: tsc

commit: git-cz

build-examples: node examples/build-examples.js

test: jest --runInBand --verbose --coverage

test-watch: jest --runInBand --watch

puml: npx puml generate flow.puml -o flow.png

release: standard-version

keywords: webpack plugin html html-webpack-plugin

testEnvironment: node

type: opencollective


code:
import { AsyncSeriesWaterfallHook } from "tapable";
import { Compiler, Compilation } from "webpack";
import { Options as HtmlMinifierOptions } from "html-minifier-terser";

export = HtmlWebpackPlugin;

declare class HtmlWebpackPlugin {
  constructor(options?: HtmlWebpackPlugin.Options);

  userOptions: HtmlWebpackPlugin.Options;

  /** Current HtmlWebpackPlugin Major */
  version: number;

  /**
   * Options after html-webpack-plugin has been initialized with defaults
   */
  options?: HtmlWebpackPlugin.ProcessedOptions;

  apply(compiler: Compiler): void;

  static getHooks(compilation: Compilation): HtmlWebpackPlugin.Hooks;

  /**
   * Static helper to create a tag object to be get injected into the dom
   */
  static createHtmlTagObject(
    tagName: string,
    attributes?: { [attributeName: string]: string | boolean },
    innerHTML?: string
  ): HtmlWebpackPlugin.HtmlTagObject;

  static readonly version: number;
}

declare namespace HtmlWebpackPlugin {
  type MinifyOptions = HtmlMinifierOptions;

  interface Options {
    /**
     * Emit the file only if it was changed.
     * @default true
     */
    cache?: boolean;
    /**
     * List all entries which should be injected
     */
    chunks?: "all" | string[];
    /**
     * Allows to control how chunks should be sorted before they are included to the html.
     * @default 'auto'
     */
    chunksSortMode?:
      | "auto"
      // `none` is deprecated and an alias for `auto` now.
      | "none"
      | "manual"
      | ((entryNameA: string, entryNameB: string) => number);
    /**
     * List all entries which should not be injected
     */
    excludeChunks?: string[];
    /**
     * Path to the favicon icon
     */
    favicon?: false | string;
    /**
     * The file to write the HTML to.
     * Supports subdirectories eg: `assets/admin.html`
     * [name] will be replaced by the entry name
     * Supports a function to generate the name
     *
     * @default 'index.html'
     */
    filename?: string | ((entryName: string) => string);
    /**
     * By default the public path is set to `auto` - that way the html-webpack-plugin will try
     * to set the publicPath according to the current filename and the webpack publicPath setting
     */
    publicPath?: string | "auto";
    /**
     * If `true` then append a unique `webpack` compilation hash to all included scripts and CSS files.
     * This is useful for cache busting
     */
    hash?: boolean;
    /**
     * Inject all assets into the given `template` or `templateContent`.
     */
    inject?:
      | false // Don't inject scripts
      | true // Inject scripts into body
      | "body" // Inject scripts into body
      | "head"; // Inject scripts into head
    /**
     * Set up script loading
     * blocking will result in <script src="..."></script>
     * defer will result in <script defer src="..."></script>
     *
     * @default 'defer'
     */
    scriptLoading?: "blocking" | "defer" | "module" | "systemjs-module";
    /**
     * Inject meta tags
     */
    meta?:
      | false // Disable injection
      | {
          [name: string]:
            | string
            | false // name content pair e.g. {viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no'}`
            | { [attributeName: string]: string | boolean }; // custom properties e.g. { name:"viewport" content:"width=500, initial-scale=1" }
        };
    /**
     * HTML Minification options accepts the following values:
     * - Set to `false` to disable minifcation
     * - Set to `'auto'` to enable minifcation only for production mode
     * - Set to custom minification according to
     * {@link https://github.com/kangax/html-minifier#options-quick-reference}
     */
    minify?: "auto" | boolean | MinifyOptions;
    /**
     * Render errors into the HTML page
     */
    showErrors?: boolean;
    /**
     * The `webpack` require path to the template.
     * @see https://github.com/jantimon/html-webpack-plugin/blob/master/docs/template-option.md
     */
    template?: string;
    /**
     * Allow to use a html string instead of reading from a file
     */
    templateContent?:
      | false // Use the template option instead to load a file
      | string
      | ((templateParameters: {
          [option: string]: any;
        }) => string | Promise<string>)
      | Promise<string>;
    /**
     * Allows to overwrite the parameters used in the template
     */
    templateParameters?:
      | false // Pass an empty object to the template function
      | ((
          compilation: Compilation,
          assets: {
            publicPath: string;
            js: Array<string>;
            css: Array<string>;
            manifest?: string;
            favicon?: string;
          },
          assetTags: {
            headTags: HtmlTagObject[];
            bodyTags: HtmlTagObject[];
          },
          options: ProcessedOptions
        ) => { [option: string]: any } | Promise<{ [option: string]: any }>)
      | { [option: string]: any };
    /**
     * The title to use for the generated HTML document
     */
    title?: string;
    /**
     * Enforce self closing tags e.g. <link />
     */
    xhtml?: boolean;
    /**
     * In addition to the options actually used by this plugin, you can use this hash to pass arbitrary data through
     * to your template.
     */
    [option: string]: any;
  }

  /**
   * The plugin options after adding default values
   */
  interface ProcessedOptions extends Required<Options> {
    filename: string;
  }

  /**
   * The values which are available during template execution
   *
   * Please keep in mind that the `templateParameter` options allows to change them
   */
  interface TemplateParameter {
    compilation: Compilation;
    htmlWebpackPlugin: {
      tags: {
        headTags: HtmlTagObject[];
        bodyTags: HtmlTagObject[];
      };
      files: {
        publicPath: string;
        js: Array<string>;
        css: Array<string>;
        manifest?: string;
        favicon?: string;
      };
      options: Options;
    };
    webpackConfig: any;
  }

  interface Hooks {
    alterAssetTags: AsyncSeriesWaterfallHook<{
      assetTags: {
        scripts: HtmlTagObject[];
        styles: HtmlTagObject[];
        meta: HtmlTagObject[];
      };
      publicPath: string,
      outputName: string;
      plugin: HtmlWebpackPlugin;
    }>;

    alterAssetTagGroups: AsyncSeriesWaterfallHook<{
      headTags: HtmlTagObject[];
      bodyTags: HtmlTagObject[];
      outputName: string;
      publicPath: string,
      plugin: HtmlWebpackPlugin;
    }>;

    afterTemplateExecution: AsyncSeriesWaterfallHook<{
      html: string;
      headTags: HtmlTagObject[];
      bodyTags: HtmlTagObject[];
      outputName: string;
      plugin: HtmlWebpackPlugin;
    }>;

    beforeAssetTagGeneration: AsyncSeriesWaterfallHook<{
      assets: {
        publicPath: string;
        js: Array<string>;
        css: Array<string>;
        favicon?: string;
        manifest?: string;
      };
      outputName: string;
      plugin: HtmlWebpackPlugin;
    }>;

    beforeEmit: AsyncSeriesWaterfallHook<{
      html: string;
      outputName: string;
      plugin: HtmlWebpackPlugin;
    }>;

    afterEmit: AsyncSeriesWaterfallHook<{
      outputName: string;
      plugin: HtmlWebpackPlugin;
    }>;
  }

  /**
   * A tag element according to the htmlWebpackPlugin object notation
   */
  interface HtmlTagObject {
    /**
     * Attributes of the html tag
     * E.g. `{'disabled': true, 'value': 'demo'}`
     */
    attributes: {
      [attributeName: string]: string | boolean | null | undefined;
    };
    /**
     * The tag name e.g. `'div'`
     */
    tagName: string;
    /**
     * The inner HTML
     */
    innerHTML?: string;
    /**
     * Whether this html must not contain innerHTML
     * @see https://www.w3.org/TR/html5/syntax.html#void-elements
     */
    voidTag: boolean;
    /**
     * Meta information about the tag
     * E.g. `{'plugin': 'html-webpack-plugin'}`
     */
    meta: {
      plugin?: string,
      [metaAttributeName: string]: any;
    };
  }
}


code:
# htmlparser2

[![NPM version](http://img.shields.io/npm/v/htmlparser2.svg?style=flat)](https://npmjs.org/package/htmlparser2)
[![Downloads](https://img.shields.io/npm/dm/htmlparser2.svg?style=flat)](https://npmjs.org/package/htmlparser2)
[![Build Status](https://img.shields.io/github/workflow/status/fb55/htmlparser2/Node.js%20Test?label=tests&style=flat)](https://github.com/fb55/htmlparser2/actions?query=workflow%3A%22Node.js+Test%22)
[![Coverage](http://img.shields.io/coveralls/fb55/htmlparser2.svg?style=flat)](https://coveralls.io/r/fb55/htmlparser2)

The fast & forgiving HTML/XML parser.

## Installation

    npm install htmlparser2

A live demo of `htmlparser2` is available [here](https://astexplorer.net/#/2AmVrGuGVJ).

## Ecosystem

| Name                                                          | Description                                             |
| ------------------------------------------------------------- | ------------------------------------------------------- |
| [htmlparser2](https://github.com/fb55/htmlparser2)            | Fast & forgiving HTML/XML parser                        |
| [domhandler](https://github.com/fb55/domhandler)              | Handler for htmlparser2 that turns documents into a DOM |
| [domutils](https://github.com/fb55/domutils)                  | Utilities for working with domhandler's DOM             |
| [css-select](https://github.com/fb55/css-select)              | CSS selector engine, compatible with domhandler's DOM   |
| [cheerio](https://github.com/cheeriojs/cheerio)               | The jQuery API for domhandler's DOM                     |
| [dom-serializer](https://github.com/cheeriojs/dom-serializer) | Serializer for domhandler's DOM                         |

## Usage

`htmlparser2` itself provides a callback interface that allows consumption of documents with minimal allocations.
For a more ergonomic experience, read [Getting a DOM](#getting-a-dom) below.

```javascript
const htmlparser2 = require("htmlparser2");
const parser = new htmlparser2.Parser({
    onopentag(name, attributes) {
        /*
         * This fires when a new tag is opened.
         *
         * If you don't need an aggregated `attributes` object,
         * have a look at the `onopentagname` and `onattribute` events.
         */
        if (name === "script" && attributes.type === "text/javascript") {
            console.log("JS! Hooray!");
        }
    },
    ontext(text) {
        /*
         * Fires whenever a section of text was processed.
         *
         * Note that this can fire at any point within text and you might
         * have to stich together multiple pieces.
         */
        console.log("-->", text);
    },
    onclosetag(tagname) {
        /*
         * Fires when a tag is closed.
         *
         * You can rely on this event only firing when you have received an
         * equivalent opening tag before. Closing tags without corresponding
         * opening tags will be ignored.
         */
        if (tagname === "script") {
            console.log("That's it?!");
        }
    },
});
parser.write(
    "Xyz <script type='text/javascript'>const foo = '<<bar>>';</ script>"
);
parser.end();
```

Output (with multiple text events combined):

```
--> Xyz
JS! Hooray!
--> const foo = '<<bar>>';
That's it?!
```

This example only shows three of the possible events.
Read more about the parser, its events and options in the [wiki](https://github.com/fb55/htmlparser2/wiki/Parser-options).

### Usage with streams

While the `Parser` interface closely resembles Node.js streams, it's not a 100% match.
Use the `WritableStream` interface to process a streaming input:

```javascript
const { WritableStream } = require("htmlparser2/lib/WritableStream");
const parserStream = new WritableStream({
    ontext(text) {
        console.log("Streaming:", text);
    },
});

const htmlStream = fs.createReadStream("./my-file.html");
htmlStream.pipe(parserStream).on("finish", () => console.log("done"));
```

## Getting a DOM

The `DomHandler` produces a DOM (document object model) that can be manipulated using the [`DomUtils`](https://github.com/fb55/DomUtils) helper.

```js
const htmlparser2 = require("htmlparser2");

const dom = htmlparser2.parseDocument();
```

The `DomHandler`, while still bundled with this module, was moved to its [own module](https://github.com/fb55/domhandler).
Have a look at that for further information.

## Parsing RSS/RDF/Atom Feeds

```javascript
const feed = htmlparser2.parseFeed(content, options);
```

Note: While the provided feed handler works for most feeds,
you might want to use [danmactough/node-feedparser](https://github.com/danmactough/node-feedparser), which is much better tested and actively maintained.

## Performance

After having some artificial benchmarks for some time, **@AndreasMadsen** published his [`htmlparser-benchmark`](https://github.com/AndreasMadsen/htmlparser-benchmark), which benchmarks HTML parses based on real-world websites.

At the time of writing, the latest versions of all supported parsers show the following performance characteristics on [Travis CI](https://travis-ci.org/AndreasMadsen/htmlparser-benchmark/builds/10805007) (please note that Travis doesn't guarantee equal conditions for all tests):

```
gumbo-parser   : 34.9208 ms/file ± 21.4238
html-parser    : 24.8224 ms/file ± 15.8703
html5          : 419.597 ms/file ± 264.265
htmlparser     : 60.0722 ms/file ± 384.844
htmlparser2-dom: 12.0749 ms/file ± 6.49474
htmlparser2    : 7.49130 ms/file ± 5.74368
hubbub         : 30.4980 ms/file ± 16.4682
libxmljs       : 14.1338 ms/file ± 18.6541
parse5         : 22.0439 ms/file ± 15.3743
sax            : 49.6513 ms/file ± 26.6032
```

## How does this module differ from [node-htmlparser](https://github.com/tautologistics/node-htmlparser)?

This module started as a fork of the `htmlparser` module.
The main difference is that `htmlparser2` is intended to be used only with node (it runs on other platforms using [browserify](https://github.com/substack/node-browserify)).
`htmlparser2` was rewritten multiple times and, while it maintains an API that's compatible with `htmlparser` in most cases, the projects don't share any code anymore.

The parser now provides a callback interface inspired by [sax.js](https://github.com/isaacs/sax-js) (originally targeted at [readabilitySAX](https://github.com/fb55/readabilitysax)).
As a result, old handlers won't work anymore.

The `DefaultHandler` and the `RssHandler` were renamed to clarify their purpose (to `DomHandler` and `FeedHandler`). The old names are still available when requiring `htmlparser2`, your code should work as expected.

## Security contact information

To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security).
Tidelift will coordinate the fix and disclosure.

## `htmlparser2` for enterprise

Available as part of the Tidelift Subscription

The maintainers of `htmlparser2` and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-htmlparser2?utm_source=npm-htmlparser2&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)


code:
import MultiplexHandler from "./MultiplexHandler";
import { Handler } from "./Parser";
export declare class CollectingHandler extends MultiplexHandler {
    private readonly cbs;
    events: [keyof Handler, ...unknown[]][];
    constructor(cbs?: Partial<Handler>);
    onreset(): void;
    restart(): void;
}
//# sourceMappingURL=CollectingHandler.d.ts.map


code:
"use strict";
var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        if (typeof b !== "function" && b !== null)
            throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
    for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
        to[j] = from[i];
    return to;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CollectingHandler = void 0;
var MultiplexHandler_1 = __importDefault(require("./MultiplexHandler"));
var CollectingHandler = /** @class */ (function (_super) {
    __extends(CollectingHandler, _super);
    function CollectingHandler(cbs) {
        if (cbs === void 0) { cbs = {}; }
        var _this = _super.call(this, function (name) {
            var _a;
            var args = [];
            for (var _i = 1; _i < arguments.length; _i++) {
                args[_i - 1] = arguments[_i];
            }
            _this.events.push(__spreadArray([name], args));
            (_a = _this.cbs[name]) === null || _a === void 0 ? void 0 : _a.apply(void 0, args);
        }) || this;
        _this.cbs = cbs;
        _this.events = [];
        return _this;
    }
    CollectingHandler.prototype.onreset = function () {
        var _a, _b;
        this.events = [];
        (_b = (_a = this.cbs).onreset) === null || _b === void 0 ? void 0 : _b.call(_a);
    };
    CollectingHandler.prototype.restart = function () {
        var _a, _b, _c;
        (_b = (_a = this.cbs).onreset) === null || _b === void 0 ? void 0 : _b.call(_a);
        for (var _i = 0, _d = this.events; _i < _d.length; _i++) {
            var _e = _d[_i], name_1 = _e[0], args = _e.slice(1);
            (_c = this.cbs[name_1]) === null || _c === void 0 ? void 0 : _c.apply(void 0, args);
        }
    };
    return CollectingHandler;
}(MultiplexHandler_1.default));
exports.CollectingHandler = CollectingHandler;


code:
import DomHandler, { DomHandlerOptions } from "domhandler";
import { ParserOptions } from "./Parser";
declare enum FeedItemMediaMedium {
    image = 0,
    audio = 1,
    video = 2,
    document = 3,
    executable = 4
}
declare enum FeedItemMediaExpression {
    sample = 0,
    full = 1,
    nonstop = 2
}
interface FeedItemMedia {
    url?: string;
    fileSize?: number;
    type?: string;
    medium: FeedItemMediaMedium | undefined;
    isDefault: boolean;
    expression?: FeedItemMediaExpression;
    bitrate?: number;
    framerate?: number;
    samplingrate?: number;
    channels?: number;
    duration?: number;
    height?: number;
    width?: number;
    lang?: string;
}
interface FeedItem {
    id?: string;
    title?: string;
    link?: string;
    description?: string;
    pubDate?: Date;
    media?: FeedItemMedia[];
}
interface Feed {
    type?: string;
    id?: string;
    title?: string;
    link?: string;
    description?: string;
    updated?: Date;
    author?: string;
    items?: FeedItem[];
}
export declare class FeedHandler extends DomHandler {
    feed?: Feed;
    /**
     *
     * @param callback
     * @param options
     */
    constructor(callback?: ((error: Error | null) => void) | DomHandlerOptions, options?: DomHandlerOptions);
    onend(): void;
}
/**
 * Parse a feed.
 *
 * @param feed The feed that should be parsed, as a string.
 * @param options Optionally, options for parsing. When using this option, you should set `xmlMode` to `true`.
 */
export declare function parseFeed(feed: string, options?: ParserOptions & DomHandlerOptions): Feed | undefined;
export {};
//# sourceMappingURL=FeedHandler.d.ts.map