JSCompiler.js

/* @flow */

import {Compiler} from './Compiler';
import {join, extname} from 'path';
import {readdir, stat, createReadStream, createWriteStream} from 'fs';
import {transformFile} from 'babel-core';
import forEach from 'lodash/forEach';
import noop from 'lodash/noop';
import {getCompiler} from './webpack';
import {babelBEOptions, isProduction} from './util';
import {logError, log, consoleStyles} from './logger';

/* eslint-disable no-sync */

const {yellow, red} = consoleStyles;

/**
 * A JavaScript compiler
 *
 * @class JSCompiler
 * @extends Compiler
 * @param {JSCompilerConfig} [options={}] - configuration object
 * @example
 * import {JSCompiler} from 'webcompiler';
 * // or - import {JSCompiler} from 'webcompiler/lib/JSCompiler';
 * // or - var JSCompiler = require('webcompiler').JSCompiler;
 * // or - var JSCompiler = require('webcompiler/lib/JSCompiler').JSCompiler;
 * import {join} from 'path';
 *
 * const srcDir = join(__dirname, 'src'),
 *   libDir = join(__dirname, 'lib');
 *
 * const compiler = new JSCompiler();
 *
 * // compile for the browser
 * compiler.fe(join(srcDir, 'script.js'), join(libDir, 'script.js'));
 *
 * // compile for Node.js
 * compiler.be(join(srcDir, 'script.js'), join(libDir, 'script.js'));
 *
 * // compile entire directories for Node.js (non-JavaScript files are simply copied over)
 * compiler.be(srcDir, libDir);
 */
export class JSCompiler extends Compiler {

  /**
   * The number of files being compiled at the moment
   *
   * @member {number} processing
   * @memberOf JSCompiler
   * @private
   * @instance
   */
  processing: number = 0;

  /**
   * Compiles a directory of files for the back end
   *
   * @memberOf JSCompiler
   * @instance
   * @private
   * @method beDir
   * @param {string}   inPath   - the input directory path
   * @param {string}   outPath  - the output directory path
   * @param {Function} callback - a callback function
   */
  beDir(inPath: string, outPath: string, callback: () => void) {
    readdir(inPath, (readdirErr, files) => {
      if (readdirErr) {
        return logError(readdirErr);
      }
      forEach(files, file => {
        this.beTraverse(join(inPath, file), join(outPath, file), callback);
      });
    });
  }

  /**
   * Compiles a JavaScript file for the back end
   *
   * @memberOf JSCompiler
   * @instance
   * @private
   * @method beFile
   * @param {string}   inPath   - the input file path
   * @param {string}   outPath  - the output file path
   * @param {Function} callback - a callback function
   */
  beFile(inPath: string, outPath: string, callback: () => void) {
    ++this.processing;
    transformFile(inPath, babelBEOptions, (transformFileErr, result) => {
      if (transformFileErr) {
        return logError(transformFileErr);
      }
      Compiler.fsWrite(outPath, result, callback);
    });
  }

  /**
   * Copies a file
   *
   * @memberOf JSCompiler
   * @instance
   * @private
   * @method copyFile
   * @param {string}   inPath   - the input file path
   * @param {string}   outPath  - the output file path
   * @param {Function} callback - a callback function
   */
  copyFile(inPath: string, outPath: string, callback: () => void) {
    ++this.processing;
    Compiler.mkdir(outPath, () => {
      createReadStream(inPath).pipe(createWriteStream(outPath));
      callback();
    });
  }

  /**
   * Compiles a JavaScript file for the back end or recursively traverses a directory, looking for the JavaScript files
   * to compile. Non-JavaScript files are simply copied over.
   *
   * @memberOf JSCompiler
   * @instance
   * @private
   * @method beTraverse
   * @param {string}   inPath   - the input file/directory path
   * @param {string}   outPath  - the output file/directory path
   * @param {Function} callback - a callback function
   */
  beTraverse(inPath: string, outPath: string, callback: () => void) {
    stat(inPath, (statErr, stats) => {
      if (statErr) {
        return logError(statErr);
      }
      if (stats.isDirectory()) {
        this.beDir(inPath, outPath, callback);
      } else if ('.js' === extname(inPath)) {
        this.beFile(inPath, outPath, callback);
      } else {
        this.copyFile(inPath, outPath, callback);
      }
    });
  }

  /**
   * Compiles a JavaScript file or a directory for the back end. Non-JavaScript files are simply copied over.
   *
   * If `inPath` is a directory, `outPath` has to be also.
   *
   * @memberOf JSCompiler
   * @instance
   * @method be
   * @param {string}   inPath                    - the input file/directory path
   * @param {string}   outPath                   - the output file/directory path
   * @param {Function} [callback=function () {}] - a callback function
   * @return {void}
   */
  be(inPath: string, outPath: string, callback: () => void = noop) {
    if (this.processing) {
      return logError(new Error('Still working'));
    }
    this.beTraverse(inPath, outPath, () => {
      --this.processing;
      if (!this.processing) {
        Compiler.done(inPath, callback);
      }
    });
  }

  /**
   * Compiles, bundles (in production mode also minifies and g-zips) a JavaScript file for the front end.
   *
   * @memberOf JSCompiler
   * @instance
   * @method fe
   * @param {string}   inPath                    - the input file path
   * @param {string}   outPath                   - the output file path
   * @param {Function} [callback=function () {}] - a callback function
   */
  fe(inPath: string, outPath: string, callback: () => void = noop) {
    const compiler = getCompiler(inPath, outPath, this.options);

    compiler.run((err, stats) => {
      if (err) {
        return logError(err);
      }
      const {warnings, errors} = stats.toJson();

      forEach(warnings, warning => {
        log(yellow(warning));
      });
      if (errors.length) {
        forEach(errors, error => {
          log(red(error));
        });

        return;
      }
      this.save(inPath, outPath, {
        code: compiler.outputFileSystem.readFileSync(outPath, 'utf8'),
        map: isProduction ? compiler.outputFileSystem.readFileSync(`${outPath}.map`, 'utf8') : ''
      }, callback);
    });
  }

}