setupHooks.js 6.17 KB
"use strict";

const webpack = require("webpack");

const {
  isColorSupported
} = require("colorette");
/** @typedef {import("webpack").Configuration} Configuration */

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

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

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

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

/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */

/** @typedef {import("../index.js").ServerResponse} ServerResponse */

/** @typedef {Configuration["stats"]} StatsOptions */

/** @typedef {{ children: Configuration["stats"][] }} MultiStatsOptions */

/** @typedef {Exclude<Configuration["stats"], boolean | string | undefined>} NormalizedStatsOptions */
// TODO remove `color` after dropping webpack v4

/** @typedef {{ children: StatsOptions[], colors?: any }} MultiNormalizedStatsOptions */

/**
 * @template {IncomingMessage} Request
 * @template {ServerResponse} Response
 * @param {import("../index.js").Context<Request, Response>} context
 */


function setupHooks(context) {
  function invalid() {
    if (context.state) {
      context.logger.log("Compilation starting...");
    } // We are now in invalid state
    // eslint-disable-next-line no-param-reassign


    context.state = false; // eslint-disable-next-line no-param-reassign, no-undefined

    context.stats = undefined;
  } // @ts-ignore


  const statsForWebpack4 = webpack.Stats && webpack.Stats.presetToOptions;
  /**
   * @param {Configuration["stats"]} statsOptions
   * @returns {NormalizedStatsOptions}
   */

  function normalizeStatsOptions(statsOptions) {
    if (statsForWebpack4) {
      if (typeof statsOptions === "undefined") {
        // eslint-disable-next-line no-param-reassign
        statsOptions = {};
      } else if (typeof statsOptions === "boolean" || typeof statsOptions === "string") {
        // @ts-ignore
        // eslint-disable-next-line no-param-reassign
        statsOptions = webpack.Stats.presetToOptions(statsOptions);
      } // @ts-ignore


      return statsOptions;
    }

    if (typeof statsOptions === "undefined") {
      // eslint-disable-next-line no-param-reassign
      statsOptions = {
        preset: "normal"
      };
    } else if (typeof statsOptions === "boolean") {
      // eslint-disable-next-line no-param-reassign
      statsOptions = statsOptions ? {
        preset: "normal"
      } : {
        preset: "none"
      };
    } else if (typeof statsOptions === "string") {
      // eslint-disable-next-line no-param-reassign
      statsOptions = {
        preset: statsOptions
      };
    }

    return statsOptions;
  }
  /**
   * @param {Stats | MultiStats} stats
   */


  function done(stats) {
    // We are now on valid state
    // eslint-disable-next-line no-param-reassign
    context.state = true; // eslint-disable-next-line no-param-reassign

    context.stats = stats; // Do the stuff in nextTick, because bundle may be invalidated if a change happened while compiling

    process.nextTick(() => {
      const {
        compiler,
        logger,
        options,
        state,
        callbacks
      } = context; // Check if still in valid state

      if (!state) {
        return;
      }

      logger.log("Compilation finished");
      const isMultiCompilerMode = Boolean(
      /** @type {MultiCompiler} */
      compiler.compilers);
      /**
       * @type {StatsOptions | MultiStatsOptions | NormalizedStatsOptions | MultiNormalizedStatsOptions}
       */

      let statsOptions;

      if (typeof options.stats !== "undefined") {
        statsOptions = isMultiCompilerMode ? {
          children:
          /** @type {MultiCompiler} */
          compiler.compilers.map(() => options.stats)
        } : options.stats;
      } else {
        statsOptions = isMultiCompilerMode ? {
          children:
          /** @type {MultiCompiler} */
          compiler.compilers.map(child => child.options.stats)
        } :
        /** @type {Compiler} */
        compiler.options.stats;
      }

      if (isMultiCompilerMode) {
        /** @type {MultiNormalizedStatsOptions} */
        statsOptions.children =
        /** @type {MultiStatsOptions} */
        statsOptions.children.map(
        /**
         * @param {StatsOptions} childStatsOptions
         * @return {NormalizedStatsOptions}
         */
        childStatsOptions => {
          // eslint-disable-next-line no-param-reassign
          childStatsOptions = normalizeStatsOptions(childStatsOptions);

          if (typeof childStatsOptions.colors === "undefined") {
            // eslint-disable-next-line no-param-reassign
            childStatsOptions.colors = isColorSupported;
          }

          return childStatsOptions;
        });
      } else {
        /** @type {NormalizedStatsOptions} */
        statsOptions = normalizeStatsOptions(
        /** @type {StatsOptions} */
        statsOptions);

        if (typeof statsOptions.colors === "undefined") {
          statsOptions.colors = isColorSupported;
        }
      } // TODO webpack@4 doesn't support `{ children: [{ colors: true }, { colors: true }] }` for stats


      if (
      /** @type {MultiCompiler} */
      compiler.compilers && statsForWebpack4) {
        /** @type {MultiNormalizedStatsOptions} */
        statsOptions.colors =
        /** @type {MultiNormalizedStatsOptions} */
        statsOptions.children.some(
        /**
         * @param {StatsOptions} child
         */
        // @ts-ignore
        child => child.colors);
      }

      const printedStats = stats.toString(statsOptions); // Avoid extra empty line when `stats: 'none'`

      if (printedStats) {
        // eslint-disable-next-line no-console
        console.log(printedStats);
      } // eslint-disable-next-line no-param-reassign


      context.callbacks = []; // Execute callback that are delayed

      callbacks.forEach(
      /**
       * @param {(...args: any[]) => Stats | MultiStats} callback
       */
      callback => {
        callback(stats);
      });
    });
  }

  context.compiler.hooks.watchRun.tap("webpack-dev-middleware", invalid);
  context.compiler.hooks.invalid.tap("webpack-dev-middleware", invalid);
  context.compiler.hooks.done.tap("webpack-dev-middleware", done);
}

module.exports = setupHooks;