Home Manual Reference Source Test Repository

lib/index.js

import Promise from 'bluebird';
import _ from 'lodash';
import moment from 'moment';
import path from 'path';
import rimraf from 'rimraf';
import ora from 'ora';
import ware from 'ware';
import cache from './cache';
import createChecksum from './checksum';
import Config from './config';
import FileSystem from './file-system';
import Url from './url';
import Metadata from './metadata';
import Renderer from './renderer';
import addCollections from './collection';
import addDataFiles from './data-files';

/**
 * Helper function to wrap commands with a log.startActivity command so we can
 * see how long a command takes.
 * @param {string} label A label to use for this command.
 * @param {Function} cmd A function to run as the command.
 */
async function wrapCommand(label, cmd) {
  const startTime = Date.now();
  const spinner = ora({
    text: label,
    spinner: 'dot4',
  }).start();

  try {
    await cmd();
    spinner.text = `${label} (${Date.now() - startTime}ms)`;
    spinner.succeed();
  } catch (e) {
    spinner.fail();
    throw e;
  }
}

/**
 * Process middleware functions.
 * @param {Array.<function>} options.middlewares Middleware functions.
 * @param {Reptar} options.reptar Reptar instance.
 * @return {Promise} Returns a Promise.
 */
function processMiddlewares({ middlewares, reptar }) {
  return new Promise((resolve, reject) => {
    ware(middlewares).run(reptar, err => {
      if (err) {
        reject(err);
        return;
      }
      resolve();
    });
  });
}

export default class Reptar {
  /**
   * Create a new Reptar instance.
   * @param {Object} options Options object.
   * @param {string} options.rootPath Where the root path of this Reptar
   *   instance points to.
   * @param {boolean} options.incremental If we should incremental build files.
   * @param {boolean} options.noTemplateCache Should templates be cached.
   *   Typically this is only off when developing or in watch mode.
   */
  constructor(options = {}) {
    /**
     * Save options passed into instance.
     * @type {Object}
     */
    this.options = _.defaults(options, {
      noTemplateCache: false,
      clean: false,
      incremental: undefined,
      rootPath: undefined,
      showSpinner: true,
    });

    this._wrapCommand = this.options.showSpinner
      ? wrapCommand
      : (label, fn) => fn();

    /**
     * Expose config object on instance.
     * @type {Config}
     */
    this.config = new Config(this.options.rootPath);

    /**
     * @type {Renderer}
     */
    this.renderer = new Renderer({
      config: this.config,
    });

    /**
     * Create backing FileSystem instance.
     * @type {FileSystem}
     */
    this.fileSystem = new FileSystem({
      config: this.config,
      renderer: this.renderer,
    });

    /**
     * Create metadata that will be accessible within every template.
     * @type {Metadata}
     */
    this.metadata = new Metadata();

    /**
     * Our destination object of where our files will be written.
     * @type {Object.<string, File>}
     */
    this.destination = Object.create(null);
  }

  async update({ skipFiles = false } = {}) {
    this.config.update();

    await this._processMiddlewares({
      middlewaresKey: 'lifecycle.willUpdate',
    });

    const { base, dir } = path.parse(this.config.root);
    cache.setNamespace(`${base}-${createChecksum(dir).slice(0, 10)}`);

    // Options passed into constructor take precedence over the config value.
    // We default to loading cache, unless explicitly set to false.
    const shouldLoadCache = !_.isNil(this.options.incremental)
      ? this.options.incremental !== false
      : this.config.get('incremental') !== false;
    if (shouldLoadCache) {
      cache.load();
    }

    Url.setSlugOptions(this.config.get('slug'));

    this.renderer.update({
      noTemplateCache: this.options.noTemplateCache,
    });

    // Expose site data from config file.
    this.metadata.set('site', this.config.get('site'));

    if (!skipFiles) {
      await this._wrapCommand(
        'Reading files.\t\t\t',
        this.fileSystem.loadIntoMemory.bind(this.fileSystem)
      );
    }

    _.forEach(this.fileSystem.files, file => {
      this.destination[file.destination] = file;
    });

    this.metadata.set('reptar', Reptar.getReptarData());

    addCollections(this);
    addDataFiles(this);

    await this._processMiddlewares({
      middlewaresKey: 'lifecycle.didUpdate',
    });

    await this._processMiddlewares({
      middlewaresKey: 'middlewares',
    });
  }

  /**
   * Removes configured destination directory and all files contained.
   * @return {Promise} Promise object.
   */
  cleanDestination() {
    return this._wrapCommand('Cleaning destination.\t\t\t', async () => {
      await Promise.fromCallback(cb => {
        rimraf(this.config.get('path.destination'), cb);
      });

      // Clear cache.
      cache.clear();
    });
  }

  /**
   * Builds the Reptar site in its entirety.
   */
  async build() {
    await this._processMiddlewares({
      middlewaresKey: 'lifecycle.willBuild',
    });

    if (this.config.get('cleanDestination') || this.options.clean) {
      await this.cleanDestination();
    }

    const metadata = this.metadata.get();

    await this._wrapCommand('Writing destination.\t\t\t', () =>
      Promise.all(_.map(this.destination, file => file.write(metadata)))
    );

    await this._processMiddlewares({
      middlewaresKey: 'lifecycle.didBuild',
    });
  }

  async _processMiddlewares({ middlewaresKey }) {
    const middlewares = this.config.get(middlewaresKey);

    if (middlewares.length === 0) {
      return;
    }

    const tabs = middlewaresKey.length > 11 ? '\t\t' : '\t\t';

    await this._wrapCommand(`Running "${middlewaresKey}".${tabs}`, () =>
      processMiddlewares({ middlewares, reptar: this })
    );
  }

  /**
   * Get information about the Reptar installation from its package.json.
   * @return {Object}
   */
  static getReptarData() {
    let packageJson = {};
    try {
      // eslint-disable-next-line
      packageJson = require(Url.pathFromRoot('./package.json'));
    } catch (e) {
      /* ignore */
    }

    return {
      version: packageJson.version,
      time: moment(new Date().getTime()).format(),
    };
  }
}