Home Reference Source

src/plugin.js

/**
 * It updates targets Babel configuration in order to add support for JSX and hot reload.
 */
class ProjextReactPlugin {
  /**
   * Class constructor.
   */
  constructor() {
    /**
     * The name of the reducer event the service will listen for in order to exclude React packages
     * from the bundle when the target is for Node or it's a browser library.
     * @type {string}
     * @access protected
     * @ignore
     */
    this._externalSettingsEventName = 'webpack-externals-configuration';
    /**
     * The list of React packages that should never end up on the bundle. For browser targets,
     * this is only added if the target is also a library.
     * @type {Array}
     * @access protected
     * @ignore
     */
    this._externalModules = [
      'react',
      'react-dom',
      'react-dom/server',
    ];
    /**
     * The name of the reducer event the service will listen for in order to add support for JSX
     * and HMR.
     * @type {string}
     * @access protected
     * @ignore
     */
    this._babelConfigurationEvent = 'babel-configuration';
    /**
     * The name of the Babel preset required to add support for React's JSX.
     * @type {string}
     * @access protected
     * @ignore
     */
    this._babelPreset = '@babel/preset-react';
    /**
     * The name of the plugin required for HMR.
     * @type {string}
     * @access protected
     * @ignore
     */
    this._babelHotPlugin = 'react-hot-loader/babel';
    /**
     * The name of the reducer event the service will listen for in order to update a target entry
     * settings when the target implements HMR.
     * @type {string}
     * @access protected
     * @ignore
     */
    this._targetEntriesEvent = 'webpack-browser-development-configuration';
    /**
     * The name of the required entry in order to enable HMR.
     * @type {string}
     * @access protected
     * @ignore
     */
    this._hotEntry = 'react-hot-loader/patch';
    /**
     * The required value a target `framework` setting needs to have in order for the service to
     * take action.
     * @type {string}
     * @access protected
     * @ignore
     */
    this._frameworkProperty = 'react';
  }
  /**
   * This is the method called when the plugin is loaded by projext. It setups all the listeners
   * for the events the plugin needs to intercept in order to add support for JSX.
   * It also listens for the event that defines the external dependencies, because if the
   * target type is Node or is a library, it should include the React packages as externals.
   * @param {Projext} app The projext main container.
   */
  register(app) {
    // Get the `events` service to listen for the events.
    const events = app.get('events');
    // Get the `babelHelper` to send to the method that updates the Babel configuration.
    const babelHelper = app.get('babelHelper');
    // Add the listener for the event that updates the Babel configuration.
    events.on(this._babelConfigurationEvent, (configuration, target) => (
      this._updateBabelConfiguration(configuration, target, babelHelper)
    ));
    // Add the listener for the event that updates the target entries.
    events.on(this._targetEntriesEvent, (config, params) => (
      this._updateTargetEntry(config, params.target)
    ));
    // Add the listener for the event that updates the external dependencies.
    events.on(this._externalSettingsEventName, (externals, params) => (
      this._updateExternals(externals, params.target)
    ));
  }
  /**
   * This method gets called when projext reduces a target Babel configuration. The method will
   * validate the target settings and add the Babel plugins needed for JSX and HMR.
   * @param {Object}      currentConfiguration The current Babel configuration for the target.
   * @param {Target}      target               The target information.
   * @param {BabelHelper} babelHelper          To update the target configuration and add the
   *                                           required preset and plugin.
   * @return {Object} The updated configuration.
   * @access protected
   * @ignore
   */
  _updateBabelConfiguration(currentConfiguration, target, babelHelper) {
    let updatedConfiguration;
    if (target.framework === this._frameworkProperty) {
      updatedConfiguration = babelHelper.addPreset(currentConfiguration, this._babelPreset);
      if (target.hot) {
        updatedConfiguration = babelHelper.addPlugin(updatedConfiguration, this._babelHotPlugin);
        updatedConfiguration = babelHelper.disableEnvPresetModules(updatedConfiguration);
      }
    } else {
      updatedConfiguration = currentConfiguration;
    }

    return updatedConfiguration;
  }
  /**
   * This method gets called when projext reduces a target configuration for Wepack. It validates
   * the target settings and if HMR is enabled, it updates the `entry` setting with the required
   * changes for the React Hot Loader.
   * @param {Object} currentConfiguration The current configuration for the target.
   * @param {Target} target               The target information.
   * @return {Object} The updated configuration.
   * @access protected
   * @ignore
   */
  _updateTargetEntry(currentConfiguration, target) {
    let updatedConfiguration;
    // If the target `framework` and `hot` have the required values...
    if (target.framework === this._frameworkProperty && target.hot) {
      // Copy the configuration.
      updatedConfiguration = Object.assign({}, currentConfiguration);
      // Update the `publicPath`, required by the loader.
      updatedConfiguration.output.publicPath = '/';
      // Get target entry name.
      const [entryName] = Object.keys(updatedConfiguration.entry);
      // Get the list of entries for the target.
      const entries = updatedConfiguration.entry[entryName];
      // Check if the `babel-polyfill` is present, since it always needs to be first.
      const polyfillIndex = entries.indexOf('babel-polyfill');
      // If the `babel-polyfill` is present...
      if (polyfillIndex > -1) {
        // ...push the required entry after it.
        entries.splice(polyfillIndex + 1, 0, this._hotEntry);
      } else {
        // ...push the required entry as the first item.
        entries.unshift(this._hotEntry);
      }
    } else {
      // ...otherwise, just set to return the received configuration.
      updatedConfiguration = currentConfiguration;
    }

    return updatedConfiguration;
  }
  /**
   * This method gets called when the webpack plugin reduces the list of modules that should be
   * handled as external dependencies. The method validates the target settings and if it's a
   * Node target or a browser library, it pushes the React packages to the list.
   * @param {Object} currentExternals A dictionary of external dependencies with the format
   *                                  webpack uses: `{ 'module': 'commonjs module'}`.
   * @param {Target} target           The target information.
   * @return {Object} The updated externals dictionary.
   * @access protected
   * @ignore
   */
  _updateExternals(currentExternals, target) {
    let updatedExternals;
    if (
      target.framework === this._frameworkProperty &&
      (target.is.node || target.library)
    ) {
      updatedExternals = Object.assign({}, currentExternals);
      this._externalModules.forEach((name) => {
        updatedExternals[name] = `commonjs ${name}`;
      });
    } else {
      updatedExternals = currentExternals;
    }

    return updatedExternals;
  }
}

module.exports = ProjextReactPlugin;