text: var async = require('async');
  var assert = require('assert');
  
  exports.normalizeLoader = function(input) {
    var normals;
  
    // happypack variant:
    if (typeof input === 'object' && input.hasOwnProperty('path')) {
      normals = [ Object.assign({}, input) ];
    }
    // webpack object variant:
    else if (typeof input === 'object') {
      normals = [].concat(extractLoaders(input));
    }
    // webpack/happypack string loader(s):
    else if (typeof input === 'string') {
      normals = [].concat(extractPathAndQueryFromString(input));
    }
  
    return normals.map(function(loader) {
      // normalize the query so that it is always a string prefixed by '?'
      if (loader.query) {
        if (typeof loader.query === 'object') {
          loader.query = '?' + JSON.stringify(loader.query);
        }
        else if (typeof loader.query === 'string') {
          loader.query = ensureHasLeadingMarker(loader.query);
        }
      }
  
      // be compatible with webpack's syntax within our own: if they specify
      // a loader's path using a "loader" attribute, map it to the "path" one
      if (loader.loader) {
        loader.path = loader.loader;
        delete loader.loader;
      }
  
      return loader;
    });
  };
  
  /**
   * Extract the set of loaders (one or more) from a given a "module.loaders"
   * webpack config entry.
   *
   * This does NOT perform any normalization on the query, so the query may be
   * a string (prefixed by '?' or not) or it may be an object.
   *
   * Example inputs/outputs:
   *
   *     // a loader chain
   *     {
   *       loaders: [ 'style', 'css' ]
   *     }
   *     // => [ "style", "css" ]
   *
   *     // another loader chain
   *     {
   *       loaders: [ 'style!css' ]
   *     }
   *     // => [ "style!css" ]
   *
   *     // a single loader, no query:
   *     {
   *       loader: 'babel'
   *     }
   *     // => [ "babel" ]
   *
   *     // a single loader with inline query:
   *     {
   *       loader: 'babel?presets[]=react'
   *     }
   *     // => [ "babel?presets[]=react" ]
   *
   *     // a single loader with a query object:
   *     {
   *       loader: 'babel',
   *       query: { presets: [ 'react' ] }
   *     }
   *     // => [ "babel?presets[]=react" ]
   *
   *     // webpack 2: multiple loaders with object / string combinations:
   *     {
   *       loaders: [
   *         'react-hot',
   *         { loader: 'babel', query: { presets: [ 'react' ] } },
   *       ]
   *     }
   *
   * @param  {Object|String} entry
   * @param  {?Array.<String|Object>} entry.loaders
   * @param  {!String} entry.loader
   * @param  {?Object|String} entry.query
   *
   * @return {Object|Array.<Object>}
   */
  function extractLoaders(entry) {
    if (typeof entry === 'object' && entry.loaders) {
      return entry.loaders.reduce(function(list, loader) {
        return list.concat(extractLoaders(loader));
      }, []);
    }
    else if (typeof entry === 'object' && entry.use) {
      return entry.use.reduce(function(list, loader) {
        return list.concat(extractLoaders(loader));
      }, []);
    }
    else if (typeof entry === 'string') {
      return extractPathAndQueryFromString(entry);
    }
    else if (typeof entry.loader === 'string' && entry.query) {
      return { path: entry.loader, query: entry.query };
    }
    else if (typeof entry.loader === 'string' && entry.options) {
      return { path: entry.loader, query: '?' + JSON.stringify(entry.options) };
    }
    else if (typeof entry.loader === 'string') {
      return extractPathAndQueryFromString(entry.loader);
    }
    else {
      console.error(entry);
      assert(false, "HappyPack: Unrecognized loader configuration variant!");
    }
  };
  
  exports.resolveLoaders = function(compiler, loaders, done) {
    async.parallel(loaders.map(function(loader) {
      assert(!!loader, "Invalid loader string to resolve!!! " + JSON.stringify(loaders));
  
      return function(callback) {
        resolveLoader(compiler.context, loader, nAry(2)(callback));
      };
    }), done);
  
    function resolveLoader(context, loader, callback) {
      if (compiler.resolverFactory) {
        compiler.resolverFactory.get("loader").resolve({}, context, loader, {}, callback);
      } else {
        var resolve = compiler.resolvers.loader.resolve;
        var resolveContext = compiler.resolvers.loader;
  
        // webpack2 has changed the signature for the resolve method where it accepts
        // a fourth argument (context), so we need to sniff and support both versions
        //
        // fixes #23
        if (resolve.length === 4) {
          resolve.apply(resolveContext, [ context, context, loader, callback ])
        }
        else {
          resolve.apply(resolveContext, [ context, loader, callback ]);
        }
      }
    }
  };
  
  function remapHook(hook) {
    return hook.replace(/\-([a-z])/g, function(_, capital) {
      return capital.toUpperCase();
    });
  }
  
  exports.tapInto = function tapInto(target, hook, callback) {
    // webpack 4 plugin
    if ('hooks' in target) {
      var tapable = target.hooks[remapHook(hook)];
  
      tapable.tap('HappyPlugin', callback);
    // webpack 1, 2, 3 plugin
    }
    else {
      target.plugin(hook, callback);
    }
  };
  
  exports.tapAsyncInto = function tapAsyncInto(target, hook, callback) {
    // webpack 4 plugin
    if ('hooks' in target) {
      var tapable = target.hooks[remapHook(hook)];
  
      tapable.tapAsync('HappyPlugin', callback);
    }
    // webpack 1, 2, 3 plugin
    else {
      target.plugin(hook, callback);
    }
  };
  
  function extractPathAndQueryFromString(string) {
    var loaders = string.split('!');
  
    if (loaders.length > 1) {
      return loaders.map(extractPathAndQueryFromString);
    }
  
    var loaderString = loaders[0];
    var fragments = loaderString.split('?');
  
    if (fragments.length === 1) {
      return { path: loaderString };
    }
  
    return { path: fragments[0], query: '?' + fragments[1] };
  }
  
  function ensureHasLeadingMarker(queryString) {
    return queryString[0] === '?' ? queryString : ('?' + queryString);
  }
  
  function nAry(n) {
    return function(f) {
      return function() {
        return f.apply(null, Array.prototype.slice.call(arguments, 0, n))
      }
    }
  }
  