Home Reference Source

src/services/server/middlewares.js

const webpack = require('webpack');
const webpackRealDevMiddleware = require('webpack-dev-middleware');
const webpackRealHotMiddleware = require('webpack-hot-middleware');
const { provider } = require('jimple');
const { deferred } = require('wootils/shared');
/**
 * This service creates and manages middlewares for webpack server implementations.
 */
class WebpackMiddlewares {
  /**
   * Class constructor.
   * @param {Events}               events               To reduce the middlewares configuration.
   * @param {Targets}              targets              To get targets information.
   * @param {WebpackConfiguration} webpackConfiguration To get a target webpack configuration in
   *                                                    order to instantiate the middlewares.
   * @param {WebpackPluginInfo}    webpackPluginInfo    To get the name of the plugin and use it
   *                                                    on the webpack hook that resolves the
   *                                                    file system when the middleware finishes
   *                                                    bundling.
   */
  constructor(events, targets, webpackConfiguration, webpackPluginInfo) {
    /**
     * A local reference for the `events` service.
     * @type {Events}
     */
    this.events = events;
    /**
     * A local reference for the `targets` service.
     * @type {Targets}
     */
    this.targets = targets;
    /**
     * A local reference for the `webpackConfiguration` service.
     * @type {WebpackConfiguration}
     */
    this.webpackConfiguration = webpackConfiguration;
    /**
     * A local reference for the plugin information.
     * @type {WebpackPluginInfo}
     */
    this.webpackPluginInfo = webpackPluginInfo;
    /**
     * A dictionary with the dev middlewares. It uses the targets names as the keys.
     * @type {Object}
     * @ignore
     * @access protected
     */
    this._devMiddlewares = {};
    /**
     * A dictionary with the hot middlewares. It uses the targets names as the keys.
     * @type {Object}
     * @ignore
     * @access protected
     */
    this._hotMiddlewares = {};
    /**
     * A dictionary of flags that indicate if a target middleware file system is ready to be used.
     * A middleware file system is not ready until webpack finishes compiling the code.
     * It uses the targets names as the keys.
     * @type {Object}
     * @ignore
     * @access protected
     */
    this._fileSystemsReady = {};
    /**
     * A dictionary of deferred promises the service uses to return when asked for a file system
     * while its middleware hasn't finished compiling.
     * It uses the targets names as the keys.
     * @type {Object}
     * @ignore
     * @access protected
     */
    this._fileSystemsDeferreds = {};
    /**
     * A dictionary of directories the middlewares use as root for their file system.
     * It uses the targets names as the keys.
     * @type {Object}
     * @ignore
     * @access protected
     */
    this._directories = {};
    /**
     * A list with the names of the targets which dev middlewares have finished compiling.
     * @type {Array}
     * @ignore
     * @access protected
     */
    this._compiled = [];
  }
  /**
   * Generate the middlewares for a given target.
   * @param {string} targetToBuild The name of the target that will be builded on the middleware(s).
   * @param {string} targetToServe The name of the target that will implement the middleware(s).
   *                               When the other target is builded, it will assume that is on the
   *                               distribution directory, and if the target serving it is being
   *                               executed from the source directory it won't be able to use the
   *                               dev middleware file system without hardcoding some relatives
   *                               paths from the build to the source; to avoid that, the method
   *                               gets the build path of this target, so when using
   *                               `getDirectory()`, it will think they are both on the
   *                               distribution directory and the paths can be created relative to
   *                               that.
   * @return {MiddlewaresInformation}
   */
  generate(targetToBuild, targetToServe) {
    // Get the target information.
    const target = this.targets.getTarget(targetToBuild);
    // Set the flag indicating the dev middleware file system is not ready.
    this._fileSystemsReady[targetToBuild] = false;
    // Create the deferred promise for when the dev middleware file system is ready.
    this._fileSystemsDeferreds[targetToBuild] = deferred();
    // Set the target working directory as the target that serves it build folder
    this._directories[targetToBuild] = this.targets.getTarget(targetToServe).paths.build;
    // Create the list of middlewares with just the dev middleware.
    const middlewares = [
      () => this._devMiddleware(target),
    ];
    // If the target uses hot replacement...
    if (target.hot) {
      // ...pubsh the function that returns the hot middleware.
      middlewares.push(() => this._hotMiddleware(target));
    }
    // Define the functions to get the file system promise and the middleware root directory.
    const getFileSystem = () => this._fileSystem(target);
    const getDirectory = () => this._directories[target.name];

    return {
      getDirectory,
      getFileSystem,
      middlewares,
    };
  }
  /**
   * Get access to a target dev middleware.
   * @param {Target} target The target for which the middleware is.
   * @return {Middleware}
   * @access protected
   * @ignore
   */
  _devMiddleware(target) {
    return this._compile(target).devMiddleware;
  }
  /**
   * Get access to a target hot middleware.
   * @param {Target} target The target for which the middleware is.
   * @return {Middleware}
   * @access protected
   * @ignore
   */
  _hotMiddleware(target) {
    return this._compile(target).hotMiddleware;
  }
  /**
   * Get access to a target dev middleware file system.
   * @param {Target} target The target owner of the middleware.
   * @return {Promise<FileSystem,Error>}
   * @access protected
   * @ignore
   */
  _fileSystem(target) {
    return this._fileSystemsReady[target.name] ?
      Promise.resolve(this._getFileSystem(target)) :
      this._fileSystemsDeferreds[target.name].promise;
  }
  /**
   * The `fileSystem` method only returns promises, but this is the one that gets the middleware
   * and returns its file system.
   * @param {Target} target The target owner of the middleware.
   * @return {FileSystem}
   * @access protected
   * @ignore
   */
  _getFileSystem(target) {
    return this._devMiddleware(target).fileSystem;
  }
  /**
   * This method gets called every time another method fromt the service needs to access a
   * middleware or a middleware property, and what it does is: Checks if the target has a compiled
   * middleware, and if it's not ready, it creates the middleware and compiles them, otherwise, it
   * just returns the saved instances.
   * This method uses the reducer event `webpack-configuration-for-middleware`, which sends the
   * middleware options, the target information, and expects an object with middleware options on
   * return.
   * @param {Target} target The target for which the middlewares are for.
   * @return {object}
   * @property {middleware}  devMiddleware An instance of the webpack dev middleware created for
   *                                       the target.
   * @property {?middleware} hotMiddleware An instance of the webpack hot middleware, if needed
   *                                       by the target.
   * @property {string}      directory     The build directory of the target implementing the
   *                                       middleware.
   * @access protected
   * @ignore
   */
  _compile(target) {
    if (!this._compiled.includes(target.name)) {
      this._compiled.push(target.name);
      const configuration = this.webpackConfiguration.getConfig(target, 'development');
      configuration.plugins.push(this._getFileSystemStatusPlugin(target));

      const compiler = webpack(configuration);
      const middlewareOptions = {
        publicPath: configuration.output.publicPath,
        stats: {
          colors: true,
          hash: false,
          timings: true,
          chunks: false,
          chunkModules: false,
          modules: false,
        },
      };
      this._devMiddlewares[target.name] = webpackRealDevMiddleware(
        compiler,
        this.events.reduce(
          'webpack-configuration-for-middleware',
          middlewareOptions,
          target
        )
      );

      if (target.hot) {
        this._hotMiddlewares[target.name] = webpackRealHotMiddleware(compiler);
      }
    }

    return {
      devMiddleware: this._devMiddlewares[target.name],
      hotMiddleware: this._hotMiddlewares[target.name],
      directory: this._directories[target.name],
    };
  }
  /**
   * Creates a _'fake webpack plugin'_ that detects when the bundle finishes compiling and let
   * the service know that the file system can accessed now.
   * @param {Target} target The target owner of the middleware.
   * @return {object} A webpack plugin.
   * @access protected
   * @ignore
   */
  _getFileSystemStatusPlugin(target) {
    return {
      apply: (compiler) => {
        const { name } = this.webpackPluginInfo;
        compiler.hooks.done.tap(`${name}-middleware`, () => {
          // Mark the file system as ready.
          this._fileSystemsReady[target.name] = true;
          // Resolve the deferred promise.
          this._fileSystemsDeferreds[target.name].resolve(
            this._fileSystem(target)
          );
        });
      },
    };
  }
}
/**
 * The service provider that once registered on the app container will set an instance of
 * `WebpackMiddlewares` as the `webpackMiddlewares` service.
 * @example
 * // Register it on the container
 * container.register(webpackMiddlewares);
 * // Getting access to the service instance
 * const webpackMiddlewares = container.get('webpackMiddlewares');
 * @type {Provider}
 */
const webpackMiddlewares = provider((app) => {
  app.set('webpackMiddlewares', () => new WebpackMiddlewares(
    app.get('events'),
    app.get('targets'),
    app.get('webpackConfiguration'),
    app.get('webpackPluginInfo')
  ));
});

module.exports = {
  WebpackMiddlewares,
  webpackMiddlewares,
};