Home Manual Reference Source

src/services/configurations/projectConfiguration.js

const ObjectUtils = require('wootils/shared/objectUtils');
const fs = require('fs-extra');
const { provider } = require('jimple');
const ConfigurationFile = require('../../abstracts/configurationFile');
/**
 * Here's the configuration with all the _'magic defaults'_ the app uses. This service generates
 * the project configuration with all settings and features projext uses.
 * This configuration is ALWAYS overwritten and extended in order to define the targets.
 * @extends {ConfigurationFile}
 */
class ProjectConfiguration extends ConfigurationFile {
  /**
   * @param {PathUtils}          pathUtils     Because `ConfigurationFile` needs it in order to
   *                                           build the overwrite path.
   * @param {Plugins}            plugins       To get the list of loaded plugins and decide the
   *                                           default build engine.
   * @param {TargetsFinder#find} targetsFinder If the configuration is not overwritten, the
   *                                           service will use this to look for existing targets
   *                                           based on the files andor folders on the source
   *                                           directory.
   */
  constructor(pathUtils, plugins, targetsFinder) {
    // Set the overwrite file path.
    super(pathUtils, [
      'projext.config.js',
      'config/projext.config.js',
      'config/project.config.js',
    ]);
    /**
     * A local reference for the `plugins` service.
     * @type {Plugins}
     */
    this.plugins = plugins;
    /**
     * A local reference for the `targetsFinder` service.
     * @type {TargetsFinder#find}
     */
    this.targetsFinder = targetsFinder;
    /**
     * The list of known build engine plugins that can be used with projext. This service
     * will validate which one is installed in order to decide the default value of the `engine`
     * settings for the targets.
     * @type {Array}
     * @access protected
     * @ignore
     */
    this._knownBuildEndgines = ['webpack', 'rollup'];
  }
  /**
   * Create the project configuration with all its _'smart defaults'_.
   * @return {ProjectConfigurationSettings}
   */
  createConfig() {
    const engine = this._getDefaultBuildEngine();
    return {
      paths: {
        source: 'src',
        build: 'dist',
        privateModules: 'private',
      },
      targetsTemplates: {
        node: {
          type: 'node',
          bundle: false,
          transpile: false,
          engine,
          hasFolder: true,
          createFolder: false,
          folder: '',
          entry: {
            default: 'index.js',
            development: null,
            production: null,
          },
          output: {
            default: {
              js: '[target-name].js',
              fonts: 'statics/fonts/[name]/[name].[hash].[ext]',
              css: 'statics/styles/[target-name].[hash].css',
              images: 'statics/images/[name].[hash].[ext]',
            },
            development: {
              fonts: 'statics/fonts/[name]/[name].[ext]',
              css: 'statics/styles/[target-name].css',
              images: 'statics/images/[name].[ext]',
            },
            production: null,
          },
          sourceMap: {
            development: false,
            production: true,
          },
          inspect: {
            enabled: false,
            host: '0.0.0.0',
            port: 9229,
            command: 'inspect',
            ndb: false,
          },
          css: {
            modules: false,
          },
          includeModules: [],
          excludeModules: [],
          includeTargets: [],
          runOnDevelopment: false,
          watch: {
            development: false,
            production: false,
          },
          babel: {
            features: {
              decorators: false,
              classProperties: false,
              dynamicImports: true,
              objectRestSpread: false,
            },
            nodeVersion: 'current',
            env: {},
            overwrites: {},
          },
          flow: false,
          typeScript: false,
          library: false,
          libraryOptions: {
            libraryTarget: 'commonjs2',
          },
          cleanBeforeBuild: true,
          copy: [],
          dotEnv: {
            enabled: true,
            files: [
              '.env.[target-name].[build-type]',
              '.env.[target-name]',
              '.env.[build-type]',
              '.env',
            ],
            extend: true,
          },
        },
        browser: {
          type: 'browser',
          engine,
          hasFolder: true,
          createFolder: true,
          folder: '',
          entry: {
            default: 'index.js',
            development: null,
            production: null,
          },
          output: {
            default: {
              js: 'statics/js/[target-name].[hash].js',
              fonts: 'statics/fonts/[name]/[name].[hash].[ext]',
              css: 'statics/styles/[target-name].[hash].css',
              images: 'statics/images/[name].[hash].[ext]',
            },
            development: {
              js: 'statics/js/[target-name].js',
              fonts: 'statics/fonts/[name]/[name].[ext]',
              css: 'statics/styles/[target-name].css',
              images: 'statics/images/[name].[ext]',
            },
            production: null,
          },
          sourceMap: {
            development: false,
            production: true,
          },
          html: {
            default: 'index.html',
            template: null,
            filename: null,
          },
          css: {
            modules: false,
            inject: false,
          },
          includeModules: [],
          excludeModules: [],
          includeTargets: [],
          uglifyOnProduction: true,
          runOnDevelopment: false,
          watch: {
            development: false,
            production: false,
          },
          babel: {
            features: {
              decorators: false,
              classProperties: false,
              dynamicImports: true,
              objectRestSpread: false,
            },
            browserVersions: 2,
            mobileSupport: true,
            polyfill: true,
            env: {},
            overwrites: {},
          },
          flow: false,
          typeScript: false,
          hot: false,
          library: false,
          libraryOptions: {
            libraryTarget: 'umd',
            compress: false,
          },
          cleanBeforeBuild: true,
          copy: [],
          dotEnv: {
            enabled: true,
            files: [
              '.env.[target-name].[build-type]',
              '.env.[target-name]',
              '.env.[build-type]',
              '.env',
            ],
            extend: true,
          },
          devServer: {
            port: 2509,
            open: true,
            reload: true,
            host: 'localhost',
            ssl: {
              key: null,
              cert: null,
              ca: null,
            },
            proxied: {
              enabled: false,
              host: null,
              https: null,
            },
            historyApiFallback: true,
          },
          configuration: {
            enabled: false,
            default: null,
            path: 'config/',
            hasFolder: true,
            defineOn: 'process.env.CONFIG',
            environmentVariable: 'CONFIG',
            loadFromEnvironment: true,
            filenameFormat: '[target-name].[configuration-name].config.js',
          },
        },
      },
      targets: {},
      copy: {
        enabled: false,
        items: [],
        copyOnBuild: {
          enabled: true,
          onlyOnProduction: true,
          targets: [],
        },
      },
      version: {
        defineOn: 'process.env.VERSION',
        environmentVariable: 'VERSION',
        revision: {
          enabled: false,
          copy: true,
          filename: 'revision',
          createRevisionOnBuild: {
            enabled: true,
            onlyOnProduction: true,
            targets: [],
          },
        },
      },
      plugins: {
        enabled: true,
        list: [],
      },
      others: {
        findTargets: {
          enabled: true,
        },
        watch: {
          poll: true,
        },
        nodemon: {
          legacyWatch: false,
        },
      },
    };
  }
  /**
   * This is the real method that creates and extends the configuration. It's being overwritten
   * for two reasons:
   * 1. In order to check if the targets finder should try to find the targets information by
   * reading the source directory or not.
   * 2. To check for custom plugins and load them.
   * @param  {Array} args A list of parameters for the service to use when creating the
   *                      configuration. This gets send from {@link ConfigurationFile#getConfig}
   * @ignore
   * @access protected
   */
  _loadConfig(...args) {
    super._loadConfig(...args);
    if (this._config.others.findTargets.enabled) {
      const originalTargets = ObjectUtils.copy(this._config.targets);
      const originalTargetsNames = Object.keys(originalTargets);
      const foundTargets = this._findTargets();
      const foundTargetsNames = Object.keys(foundTargets);
      /**
       * If there's only one target on the configuration file and the finder only found one, the
       * name of the found one will be changed to the one on the configuration file.
       *
       * When a single target, outside a folder, is found, the finder will give it the same name
       * as the project name on the `package.json`, but by defining a single target on the
       * configuration file, the name can be changed.
       */
      if (originalTargetsNames.length === 1 && foundTargetsNames.length === 1) {
        const [originalTargetName] = originalTargetsNames;
        const [foundTargetName] = foundTargetsNames;
        if (originalTargetName !== foundTargetName) {
          foundTargets[originalTargetName] = foundTargets[foundTargetName];
          foundTargets[originalTargetName].name = originalTargetName;
          delete foundTargets[foundTargetName];
        }
      }
      this._config.targets = ObjectUtils.merge(foundTargets, originalTargets);
    }
    // If custom plugins are enabled...
    if (this._config.plugins.enabled) {
      /**
       * First check if one of the _"known plugins"_ exist, then append the list of plugins
       * defined on the configuration and finally try to load them.
       */
      this._validatePlugins(this._config.plugins.list)
      .forEach((pluginFile) => this.plugins.loadFromFile(pluginFile));
    }
  }
  /**
   * It tries to find basic targets information by reading the source directory.
   * @return {Object} If there were targets to be found, this will be a dictionary of
   *                  {@link TargetsFinderTarget}, with the targets name as keys.
   * @ignore
   * @access protected
   */
  _findTargets() {
    const result = {};
    this.targetsFinder(this._config.paths.source)
    .forEach((target) => {
      result[target.name] = target;
    });

    return result;
  }
  /**
   * Gets the name of the default build engine that the service will use as default for the
   * targets templates. It finds the name by using a list of known engines and checking if any of
   * them was loaded as a plugin.
   * @return {string}
   * @access protected
   * @ignore
   */
  _getDefaultBuildEngine() {
    return this._knownBuildEndgines.find((engine) => this.plugins.loaded(engine));
  }
  /**
   * This method validates if any of the _"known plugin paths"_ exists and add them to the top
   * of the list.
   * @param {Array} definedPlugins The list of plugin paths defined on the configuration.
   * @return {Array}
   * @access protected
   * @ignore
   */
  _validatePlugins(definedPlugins) {
    const knownPlugins = [
      'projext.plugin.js',
      'config/projext.plugin.js',
    ];

    return [
      ...knownPlugins.filter((pluginPath) => fs.pathExistsSync(this.pathUtils.join(pluginPath))),
      ...definedPlugins,
    ];
  }
}
/**
 * The service provider that once registered on the app container will set an instance of
 * `ProjectConfiguration` as the `projectConfiguration` service.
 * @example
 * // Register it on the container
 * container.register(projectConfiguration);
 * // Getting access to the service instance
 * const projectConfiguration = container.get('projectConfiguration');
 * @type {Provider}
 */
const projectConfiguration = provider((app) => {
  app.set('projectConfiguration', () => new ProjectConfiguration(
    app.get('pathUtils'),
    app.get('plugins'),
    app.get('targetsFinder')
  ));
});

module.exports = {
  ProjectConfiguration,
  projectConfiguration,
};