contentascode/docsmith

View on GitHub
src/docsmith-install.js

Summary

Maintainability
D
1 day
Test Coverage
#!/usr/bin/env node

/**
 * Module dependencies.
 */

const _ = require('lodash');
const program = require('commander');
const templates = require('./docsmith/templates');
const settings = require('./docsmith/utils/settings');
// const components = require('../components');
const yaml = require('js-yaml');
const fs = require('fs-extra');
const path = require('path');
const exec = require('child_process').execFile;

const id = x => x;
const cp = require('child_process');
const git = require('nodegit');

const curSettings = settings.config;

let component, gh_token;

program
  .description('install one or more components with their default settings or a specific plugin')
  .option('-f, --force', 'Initialise whether the current directory is empty or not.', id, false)
  .option('--git-name <git_name>', 'Overrides the current git user name or GIT_NAME env variable for the travis plugin')
  .option('--git-email <git_email>', 'Overrides the current git email or GIT_EMAIL env variablefor the travis plugin')
  .option('--gh-token <github_token>', 'Overrides GH_TOKEN env variable for the travis plugin')
  .option('--test', 'Bypasses specific tasks for test runs')
  .arguments('[component] [plugin]')
  .action(function(comp, plug, options) {
    // Temporary fix for https://github.com/tj/commander.js/issues/508
    if (options.test && plug) {
      plugin = comp;
      component = plug;
    } else {
      component = comp;
      plugin = plug;
    }
  })
  .parse(process.argv);

let newSettings, plugin;

if (!program.force) {
  console.log('EXPERIMENTAL -  This is probably not working. Use --force to bypass this warning.');
  process.exit(0);
}

if (!curSettings.integrate) curSettings.integrate = {};

switch (component) {
  case 'travis': {
    // TODO: Check if there are any needed updates. This should probably just be a call to a trusted build system
    // For now just rerun configuration creation all the time.

    // if (!(plugin in curSettings.integration)) {
    const TRUE = true;
    if (TRUE) {
      curSettings.integrate.travis = settings.DEFAULT_TRAVIS;

      if (!program.gh_token) {
        if (!process.env.GH_TOKEN && !program.test) {
          console.log('Travis requires a Github Authentication Token in order to publish your website to Github Pages');
          console.log('The GH_TOKEN environment variable needs to be set, or the --gh-token option needs to be used.');
          process.exit(1);
        } else {
          gh_token = process.env.GH_TOKEN;
        }
      } else {
        gh_token = program.gh_token;
      }

      // Install and check necessary files - .travis.yml
      //
      // For now this is a blend of trying to generate files, do idempotent file check a la ansible
      // and just copying template files.
      //
      // Later for moving between configurations, things might be different. Using npm build for now.
      //
      // Maybe this should be parameterised to be able to change build system and compose different components.
      // One of the big factors is if there is a linear build pipe a la metalsmith, or if we have a tree which will
      // necessitate more of a Make or equivalent approach.

      create_travis_yml(gh_token)
        .then(jekyll_config('../components/travis/_config.yml', '_config.yml'))
        .then(npm_build('../components/travis/npm_build.yml', 'package.json'))
        .then(function() {
          console.log('You have just installed travis');
          console.log('You will need to:');
          console.log(' - Push this folder to your repo (soon with `content save`)');
          console.log(' - Activate travis for your repository');
        })
        .catch(
          // Log the rejection reason
          function(reason) {
            console.log('Problem installing the travis build component');
            console.log(reason);
            process.exit(1);
          }
        );

      newSettings = curSettings;
    }

    break;
  }
  case 'github-pages': {
    if (!(plugin in curSettings.integrate)) {
      curSettings.integrate.travis = settings.DEFAULT_GITHUB_PAGES;
      newSettings = curSettings;
    }
    break;
  }
  case 'validate': {
    switch (plugin) {
      case 'links': {
        console.log('Not implemented yet');
        process.exit();
        break;
      }
      default: {
        if (curSettings.integrate == {}) {
          console.log('Current integration plugin configuration is');
          console.log(curSettings.integrate);
          process.exit();
        } else {
          console.log(
            'No integration plugin currently installed. Please specify a plugin to install a build component.'
          );
          console.log('For instance to enable the travis plugin use :');
          console.log('$ content install travis');
          process.exit();
        }
      }
    }
    const install_integration = () => {
      //TODO
      return;
    };
    newSettings = install_integration(plugin, gh_token, curSettings);
    break;
  }
  // default:
  //   console.log('%s is not a known component.', component);
  //   process.exit();
}

// update settings file.

if (newSettings) {
  settings.save(newSettings);
  console.log('Saved new component');
  //console.log(newSettings.integration)
} else {
  // TODO Check that currently installed plugin configuration is sane.
  console.log('No modifications. Current settings are');
  console.log(curSettings);
}

function create_travis_yml(gh_token) {
  return new Promise(function(resolve, reject) {
    // This builds the yaml in memory including the secure token and writes the file
    // It could also use a moustache style template in integration/travis/.travis.yml

    // TODO: Refactor to separate file merge from token generation. Use Object.assign approach like for npm_build.

    const travis_yml = yaml.safeLoad(
      fs.readFileSync(path.join(templates.path, '../components/travis/.travis.yml'), 'utf8')
    );

    let git_name, git_email;

    const promise_name = git.Config
      .openDefault()
      .then(function(config) {
        return config.getString('user.name');
      })
      .catch(function(err) {
        console.log('Problem with git configuration. Have set your user.email? You can use:');
        console.log("git config user.name 'Your Name'");
        console.log(err);
      });

    const promise_email = git.Config
      .openDefault()
      .then(function(config) {
        return config.getString('user.email');
      })
      .catch(function(err) {
        console.log('Problem with git configuration. Have set your user.email? You can use:');
        console.log("git config user.email 'you@email.net'");
        console.log(err);
      });

    return Promise.all([promise_name, promise_email])
      .then(function(values) {
        let token = 'TOKEN';

        git_name = process.env.GIT_NAME || values[0];
        git_email = process.env.GIT_EMAIL || values[1];

        travis_yml.env.global = [];

        travis_yml.env.global.push('GH_USERNAME=' + process.env.GH_USERNAME);
        travis_yml.env.global.push('CONFIG_OWNER=' + process.env.CONFIG_OWNER);
        travis_yml.env.global.push('CONFIG_REPO=' + process.env.CONFIG_REPO);

        // Generate the travis encrypted variable to access Github.
        try {
          if (!curSettings.offline && !program.test) {
            //console.log("travis encrypt \'GIT_NAME=\"" + git_name + "\" GIT_EMAIL=\"" + git_email + "\" GH_TOKEN=\"" + gh_token + "\"\'")
            const stdout = cp.execSync(
              'travis encrypt \'GIT_NAME="' + git_name + '" GIT_EMAIL="' + git_email + '" GH_TOKEN="' + gh_token + '"\''
            );
            token = stdout.toString();
          }
          travis_yml.env.global.push({ secure: token });
          resolve();
        } catch (e) {
          console.log(
            'You need to have a working ruby environment and have installed the travis gem with `gem install travis`'
          );
          reject(e);
        }

        // write the .travis.yml file.
        try {
          fs.writeFileSync('./.travis.yml', yaml.safeDump(travis_yml), 'utf8');
          resolve();
        } catch (e) {
          reject(e);
        }
      })
      .catch(function(err) {
        console.log(err);
      });
  });
}

// function lineinfile(dest, line) {
//   return new Promise(function(resolve, reject) {
//     // mimicking ansible lineinfile module API with state=present
//     fs.readFile(dest, function(err, data) {
//       if (err) reject(err);
//       if (data.toString().indexOf(line) > -1) {
//         // Should use ansible regexp feature to deal with versions.
//         resolve();
//       } else {
//         fs.appendFile(dest, '\n' + line, function(err) {
//           if (err) reject();
//           console.log(dest + ' ' + line + ' added.');
//           resolve();
//         });
//       }
//     });
//   });
// }

function npm_build(src, dest) {
  const load_npm_build_yaml = new Promise(function(resolve) {
    resolve(yaml.safeLoad(fs.readFileSync(path.join(templates.path, src), 'utf8')));
  });

  const read_package_json = function(yaml) {
    const check_package_json = new Promise(function(resolve, reject) {
      fs.stat(dest, function(err, stats) {
        if (err) {
          resolve(false);
        } else if (stats.isFile()) {
          resolve(true);
        } else {
          reject(dest + ' is not a file');
        }
      });
    });

    return new Promise(function(resolve, reject) {
      function promiseFromChildProcess(child) {
        return new Promise(function(resolve, reject) {
          child.addListener('error', reject);
          child.addListener('exit', resolve);
        });
      }

      const child = exec('npm', ['init', '-f'], { env: process.env });

      // promiseFromChildProcess(child).then(function (result) {
      //     console.log('promise complete: ' + result);
      // }, function (err) {
      //     console.log('promise rejected: ' + err);
      // });

      // child.stdout.on('data', function (data) {
      //     console.log('stdout: ' + data);
      // });
      // child.stderr.on('data', function (data) {
      //     console.log('stderr: ' + data);
      // });
      // child.on('close', function (code) {
      //     console.log('closing code: ' + code);
      // });

      return check_package_json
        .then(exists => {
          if (!exists) {
            // console.log(dest + ': does not exist!')
            return promiseFromChildProcess(child);
          }
        })
        .then(() => {
          fs.readFile(dest, function(err, data) {
            if (err) reject(err);
            console.log(dest + ': exists!');
            const pkg = JSON.parse(data.toString());
            pkg.author = pkg.author || 'Unknown';
            resolve([yaml, pkg]);
          });
        });
    });
  };

  // Not using bluebird and spread yet...
  const write_package_json = function(val) {
    return new Promise(function(resolve, reject) {
      fs.writeFile(dest, JSON.stringify(_.merge(val[1], val[0]), null, '  '), 'utf8', function(err) {
        if (err) reject(err);
        console.log(dest + ' updated');
        resolve();
      });
    });
  };

  return load_npm_build_yaml.then(read_package_json).then(write_package_json);
}

function jekyll_config(src, dest) {
  return new Promise(function(resolve, reject) {
    const jekyll_config = yaml.safeLoad(fs.readFileSync(path.join(templates.path, src), 'utf8'));

    fs.readFile(dest, function(err, data) {
      let _config = {};
      if (err) {
        if (err.code != 'ENOENT') {
          reject(err);
          return;
        }
      } else {
        _config = yaml.safeLoad(data.toString());
      }

      jekyll_config.url = 'http://' + process.env.CONFIG_OWNER + '.github.io';
      jekyll_config.baseurl = '/' + process.env.CONFIG_REPO;
      jekyll_config.github.repository_url =
        'https://github.com/' + process.env.CONFIG_OWNER + '/' + process.env.CONFIG_REPO;

      fs.writeFile(dest, yaml.safeDump(_.merge(_config, jekyll_config), null, '  '), 'utf8', function(err) {
        if (err) reject(err);
        console.log(dest + ' updated');
        resolve();
      });
    });
  });
}

// function copyfile(src, dest) {
//   // fs.copySync(path.join(templates.path, src), path.join(process.cwd(), src));
//
//   return new Promise(function(resolve, reject) {
//     fs.copy(path.join(templates.path, src), path.join(process.cwd(), dest), function(err) {
//       if (err) reject(err);
//       console.log(path.join(templates.path, src) + ' copied in ' + path.join(process.cwd(), src));
//       resolve();
//     });
//   });
// }