contentascode/docsmith

View on GitHub
src/docsmith/utils/metalsmith.js

Summary

Maintainability
B
6 hrs
Test Coverage
// Extracted from metalsmith library to handle config loading
// Modified to:
//  - support config file overrides.
//  - process files to symlinks instead of building

const debug = require('debug')('docsmith:metalsmith');
const chalk = require('chalk');
const resolve = require('path').resolve;
const exists = require('fs').existsSync;
const read = require('fs').readFileSync;
const basename = require('path').basename;
const extname = require('path').extname;
const dirname = require('path').dirname;
const format = require('path').format;
const yaml = require('js-yaml').safeLoad;
const async = require('async');
// const fs = require('co-fs-extra');
const path = require('path');
const _ = require('lodash');

// const absolute = require('absolute');
// const unyield = require('unyield');
const Metalsmith = require('metalsmith');
const debugui = require('metalsmith-debug-ui');

// Pass config file path, config overrides and make async.
module.exports = function(config, overrides, callback) {
  const name = basename(config, extname(config));
  debug('name', name);
  const dir = resolve(process.cwd(), dirname(config));
  debug('dir', dir);

  let json;

  ['.json', '.yml', '.yaml'].forEach(function(ext) {
    const conf = format({ root: '/', dir, base: name + ext });
    const path = resolve(dir, dirname(config), conf);

    if (!exists(path) || json) return;
    try {
      if (ext === '.json') {
        json = require(path);
      } else if (ext === '.yml' || ext === '.yaml') {
        json = yaml(read(path, 'utf8'));
      }
    } catch (e) {
      return callback(fatal('it seems like ' + conf + ' is malformed.', new Error('Error in conf', conf)));
    }
  });

  if (!json)
    return callback(
      fatal(
        'could not find a ' + config.replace('.json', '') + '.json / .yml / .yaml configuration file.',
        new Error('Could not find configuration file')
      )
    );

  const metalsmith = new Metalsmith(dir);

  const {
    source = json.source,
    destination = json.destination,
    concurrency = json.concurrency,
    metadata = json.metadata,
    clean = json.clean,
    frontmatter = json.frontmatter,
    ignore = json.ignore,
    plugins = [],
    dbg
  } = overrides;

  if (dbg) debugui.patch(metalsmith, { perf: true });

  if (source) metalsmith.source(source);
  if (destination) metalsmith.destination(destination);
  if (concurrency) metalsmith.concurrency(concurrency);
  if (metadata) metalsmith.metadata(json.metadata ? _.merge(json.metadata, metadata) : metadata);

  if (clean !== undefined) metalsmith.clean(clean);
  if (frontmatter !== undefined) metalsmith.frontmatter(frontmatter);
  if (ignore !== undefined) metalsmith.ignore(ignore);

  debug('json.plugins', json.plugins);
  debug('plugins', plugins);
  json.plugins = json.plugins.concat(plugins);

  /**
   * Plugins.
   */

  async.eachSeries(
    normalize(json.plugins),
    function(plugin, cb) {
      for (const name in plugin) {
        const opts = plugin[name];
        let mod;

        try {
          const local = resolve(dir, name);
          const npm = resolve(dir, 'node_modules', name);
          debug('resolving npm package with :', path.join(dir, 'node_modules', name));
          // debug('ls ' + dir, fs.readdirSync(dir).join('\n'));
          if (exists(local) || exists(local + '.js')) {
            mod = require(local);
          } else if (exists(npm)) {
            mod = require(npm);
          } else {
            mod = require(name);
          }
        } catch (e) {
          return cb(fatal('failed to require plugin "' + name + '".', new Error('Could not find plugin')));
        }

        try {
          metalsmith.use(mod(opts));
          cb();
        } catch (e) {
          return cb(fatal('error using plugin "' + name + '"...', e.message + '\n\n' + e.stack));
        }
      }
    },
    err => {
      if (err) return callback(err);
      return;
    }
  );

  /**
   * TODO: Monkey patch to create symlinked build?
   */

  // metalsmith.writeFile = unyield(function*(file, data) {
  //   const dest = this.destination();
  //   if (!absolute(file)) file = path.resolve(dest, file);
  //
  //   try {
  //     yield fs.outputFile(file, data.contents);
  //     if (data.mode) yield fs.chmod(file, data.mode);
  //   } catch (e) {
  //     e.message = 'Failed to write the file at: ' + file + '\n\n' + e.message;
  //     throw e;
  //   }
  // });

  /**
   * Build.
   */

  metalsmith.build(function(err) {
    if (err) return callback(fatal(err.message, err));
    log('successfully built files.');

    return callback();
  });

  /**
   * Log an error and then exit the process.
   *
   * @param {String} msg
   * @param {String} [stack]  Optional stack trace to print.
   */

  function fatal(msg, err) {
    console.error();
    console.error(chalk.red('  Metalsmith') + chalk.gray(' · ') + msg);
    if (err.stack) {
      console.error();
      console.error(chalk.gray(err.stack));
    }
    console.error();
    return err;
  }

  /**
   * Log a `message`.
   *
   * @param {String} message
   */

  function log(message) {
    console.log();
    console.log(chalk.gray('  Metalsmith · ') + message);
    console.log();
  }

  /**
   * Normalize an `obj` of plugins.
   *
   * @param {Array or Object} obj
   * @return {Array}
   */

  function normalize(obj) {
    if (obj instanceof Array) return obj;
    const ret = [];

    for (const key in obj) {
      const plugin = {};
      plugin[key] = obj[key];
      ret.push(plugin);
    }

    return ret;
  }
};