Home Reference Source

src/plugins/copy/index.js

const path = require('path');
const fs = require('fs-extra');
const extend = require('extend');
const { deferred } = require('wootils/shared');
/**
 * This is a Rollup plugin that copies specific files during the bundling process.
 */
class ProjextRollupCopyPlugin {
/**
 * @param {ProjextRollupCopyPluginOptions} [options={}]
 * The options to customize the plugin behaviour.
 * @param {string} [name='projext-rollup-plugin-copy']
 * The name of the plugin's instance.
 */
  constructor(options, name = 'projext-rollup-plugin-copy') {
    /**
     * The plugin options.
     * @type {ProjextRollupCopyPluginOptions}
     * @access protected
     * @ignore
     */
    this._options = extend(
      true,
      {
        files: [],
        stats: () => {},
      },
      options
    );
    /**
     * The name of the plugin's instance.
     * @type {string}
     */
    this.name = name;
    // Validate the received options before doing anything else.
    this._validateOptions();
    /**
     * A list of the directories the plugin created while copying files. This list exists in order
     * to prevent the plugin from trying to create the same directory more than once.
     * @type {Array}
     * @access protected
     * @ignore
     */
    this._createdDirectoriesCache = [];
    /**
     * @ignore
     */
    this.writeBundle = this.writeBundle.bind(this);
  }
  /**
   * Gets the plugin options
   * @return {ProjextRollupCopyPluginOptions}
   */
  getOptions() {
    return this._options;
  }
  /**
   * This is called after Rollup finishes writing the files on the file system. This is where
   * the plugin will filter the files that doesn't exist and copy the rest.
   * @return {Promise<Array,Error>} The resolved array has the path for each copied file.
   */
  writeBundle() {
    // Reset the _"cache"_.
    this._createdDirectoriesCache = [];
    return Promise.all(
      // Loop all the files to copy.
      this._options.files
      // Remove those which origin file doesn't exist.
      .filter((fileInfo) => fs.pathExistsSync(fileInfo.from))
      // Copy the rest...
      .map((fileInfo) => {
        // Make sure the output directory exists.
        this._ensurePath(fileInfo.to);
        // _"Copy"_ the file.
        return this._copy(fileInfo);
      })
    );
  }
  /**
   * Valiates the plugin options.
   * @throws {Error} If a file doesn't have a `from` and/or `to` property.
   * @access protected
   * @ignore
   */
  _validateOptions() {
    const invalid = this._options.files.find((fileInfo) => !fileInfo.from || !fileInfo.to);
    if (invalid) {
      throw new Error(`${this.name}: All files must have 'from' and 'to' properties`);
    }
  }
  /**
   * This is the real method that _"copies"_ a file. If a `transform` property was defined,
   * instead of copying the file, it will read it from the source, send it to the transform
   * function and then write it on the destination path.
   * @param {ProjextRollupCopyPluginItem} fileInfo The information of the file to copy.
   * @return {Promise<string,Error>} The resolved value is the path where the file was copied.
   * @access protected
   * @ignore
   */
  _copy(fileInfo) {
    // Get the file information.
    const { from, to, transform } = fileInfo;
    // Get a deferred for the stats entry.
    const statsDeferred = deferred();
    // Add the deferred promise on the stats.
    this._options.stats(statsDeferred.promise);
    /**
     * If a `transform` property was defined, call the method that handles that, otherwise, just
     * copy the file.
     */
    const firstStep = transform ?
      this._transformFile(fileInfo) :
      fs.copy(from, to);

    return firstStep
    .then(() => {
      // Resolve the deferred for the stats entry.
      statsDeferred.resolve({
        plugin: this.name,
        filepath: to,
      });
      // Resolve the path where the file was copied.
      return to;
    });
  }
  /**
   * This is called when a file is about to be copied but it has a `transform` property.
   * The method will read the file contents, call the function on the `transform` property and
   * then write the _"transformed"_ code on the path where the file should be copied.
   * @param {ProjextRollupCopyPluginItem} fileInfo The information of the file to copy.
   * @return {Promise<undefined,Error>}
   * @access protected
   * @ignore
   */
  _transformFile(fileInfo) {
    // Read the file.
    return fs.readFile(fileInfo.from)
    // _"Transform it"_.
    .then((contents) => fileInfo.transform(contents))
    // Write the new file.
    .then((contents) => fs.writeFile(fileInfo.to, contents));
  }
  /**
   * This is called before trying to copy any file. The method makes sure a path exists so a file
   * can be copied. It also checks with the _"internal cache"_ to make sure the directory wasn't
   * already created.
   * @param {string} filepath A filepath from which the method will take the directory in order to
   *                          verify it exists.
   * @access protected
   * @ignore
   */
  _ensurePath(filepath) {
    const directory = path.dirname(filepath);
    if (!this._createdDirectoriesCache.includes(directory)) {
      this._createdDirectoriesCache.push(directory);
      fs.ensureDirSync(directory);
    }
  }
}
/**
 * Shorthand method to create an instance of {@link ProjextRollupCopyPlugin}.
 * @param {ProjextRollupCopyPluginOptions} options
 * The options to customize the plugin behaviour.
 * @param {string} name
 * The name of the plugin's instance.
 * @return {ProjextRollupCopyPlugin}
 */
const copy = (options, name) => new ProjextRollupCopyPlugin(options, name);

module.exports = {
  ProjextRollupCopyPlugin,
  copy,
};