SASSCompiler.js

/* @flow */

import type {ProgramData, ProgramDataCallback, SASSCompilerConfig} from './typedef';
import {Compiler} from './Compiler';
import {render} from 'node-sass';
import importer from 'node-sass-import-once';
import postcss from 'postcss';
import autoprefixer from 'autoprefixer';
import noop from 'lodash/noop';
import {logError, logPostCSSWarnings, logSASSError} from './logger';

const precision = 8,
  importOnceDefaults = {index: true, css: false, bower: false},
  defaultIncludePaths = [
    'node_modules/bootstrap-sass/assets/stylesheets',
    'node_modules/font-awesome/scss',
    'node_modules',
    'node_modules/bootswatch'
  ],
  defaultOptions = {
    includePaths: [],
    importOnce: {}
  };

/**
 * A SASS compiler
 *
 * Configures the default include paths for the following popular CSS modules:
 *
 *  1. Bootstrap (e.g. you can write `@import "bootstrap";` in your ".scss" files)
 *  2. Font Awesome (`@import "font-awesome";`)
 *  3. Bootswatch (`@import "cosmo/variables"; @import "bootstrap"; @import "cosmo/bootswatch";`)
 *
 * Additionally, if an NPM module contains an `_index.scss` (or `_index.sass`, or `index.scss`, or `index.sass`) file in
 * its root directory, importing its stylesheets is as easy as: `@import "<module name>";` (same as you would `import`
 * the module in JavaScript).
 *
 * @class SASSCompiler
 * @extends Compiler
 * @param {SASSCompilerConfig} [options={}] - configuration object
 * @example
 * import {SASSCompiler} from 'webcompiler';
 * // or - import {SASSCompiler} from 'webcompiler/lib/SASSCompiler';
 * // or - var SASSCompiler = require('webcompiler').SASSCompiler;
 * // or - var SASSCompiler = require('webcompiler/lib/SASSCompiler').SASSCompiler;
 * import {join} from 'path';
 *
 * const scssDir = join(__dirname, 'scss'),
 *   cssDir = join(__dirname, 'css');
 *
 * const compiler = new SASSCompiler();
 *
 * // compile for the browser
 * compiler.fe(join(scssDir, 'style.scss'), join(cssDir, 'style.css'));
 */
export class SASSCompiler extends Compiler {

  /**
   * postcss plugins
   *
   * @member {Array<*>} postcssPlugins
   * @memberof SASSCompiler
   * @private
   * @instance
   */
  postcssPlugins: any[] = [autoprefixer];

  // eslint-disable-next-line require-jsdoc
  constructor(options: SASSCompilerConfig = {}) {
    const {includePaths, importOnce, ...rest} = {...defaultOptions, ...options};

    super({
      includePaths: defaultIncludePaths.concat(includePaths),
      importOnce: {...importOnceDefaults, ...importOnce},
      ...rest
    });
  }

  /**
   * Adds postcss plugins (autoprefixer is included by default).
   *
   * @memberof SASSCompiler
   * @instance
   * @method addPostcssPlugins
   * @param {...*} plugins - postcss plugins
   * @return {SASSCompiler} self
   */
  addPostcssPlugins(...plugins: any[]) {
    this.postcssPlugins.push(...plugins);

    return this;
  }

  /**
   * Runs the compiled code through the list of configured postcss plugins.
   *
   * @memberOf SASSCompiler
   * @instance
   * @method postcss
   * @param {string}              path     - a path to the file
   * @param {ProgramData}         data     - the actual program data to execute postcss on
   * @param {ProgramDataCallback} callback - a callback function
   */
  postcss(path: string, data: ProgramData, callback: ProgramDataCallback) {
    postcss(this.postcssPlugins).process(data.code, {
      from: path,
      to: path,
      map: {prev: data.map}
    }).then(result => {
      const warnings = result.warnings();

      if (warnings.length) {
        return logPostCSSWarnings(warnings);
      }
      callback({code: result.css, map: JSON.stringify(result.map)});
    }, logError);
  }

  /**
   * Compiles, runs postcss on the result and, optionally, g-zips in the production mode.
   *
   * @memberof SASSCompiler
   * @instance
   * @method fe
   * @param {string}   inPath                    - a full system path to the input file
   * @param {string}   outPath                   - a full system path to the output file
   * @param {Function} [callback=function () {}] - a callback function
   */
  fe(inPath: string, outPath: string, callback: () => void = noop) {
    const {importOnce, includePaths} = this.options;

    render({
      file: inPath,
      outFile: outPath,
      importer,
      precision,
      importOnce,
      includePaths,
      sourceMap: true,
      sourceMapContents: true,
      outputStyle: 'compressed'
    }, (error, result) => {
      if (error) {
        return logSASSError(error);
      }
      this.postcss(outPath, {code: result.css, map: result.map.toString()}, data => {
        this.save(inPath, outPath, data, callback);
      });
    });
  }

}