webgme/webgme-cli

View on GitHub
src/utils.js

Summary

Maintainability
C
1 day
Test Coverage
/*globals define*/
"use strict";

const childProcess = require("child_process");
const spawn = childProcess.exec;
const Logger = require("./Logger");
const logger = new Logger();
const PROJECT_CONFIG = "webgme-setup.json";

var _ = require("lodash"),
  fs = require("fs"),
  path = require("path"),
  exists = require("exists-file"),
  assert = require("assert"),
  R = require("ramda");

var getRootPath = function (startPath) {
  // Walk back from current path until you find a webgme-setup.json file
  var abspath = path.resolve(startPath || "."),
    previousPath;

  while (abspath !== previousPath) {
    if (isProjectRoot(abspath)) {
      return abspath;
    }
    previousPath = abspath;
    abspath = path.dirname(abspath);
  }
  return null;
};

var isProjectRoot = function (abspath) {
  // Check for webgme-setup.json file
  if (!exists(abspath)) {
    return null;
  }

  var files = fs.readdirSync(abspath);
  return (
    files.filter(function (file) {
      return file === PROJECT_CONFIG;
    }).length > 0
  );
};

var changeToRootDir = function (startPath) {
  // Check for project directory
  var rootPath = getRootPath(startPath);

  if (rootPath === null) {
    var err = "Could not find a project in current or any parent directories";
    throw new Error(err);
  }

  process.chdir(rootPath);
};

/**
 * Save file and create directories as needed.
 *
 * @param {File} file
 * @return {undefined}
 */
var saveFile = function (file) {
  var dir = path.dirname(file.name);

  createDir(dir);
  fs.writeFileSync(file.name, file.content);
};

/**
 * Create directory as needed.
 *
 * @param {String} dir
 * @return {undefined}
 */
var createDir = function (dir) {
  var dirs = path.resolve(dir).split(path.sep),
    shortDir,
    i = 1;

  while (i++ < dirs.length) {
    shortDir = dirs.slice(0, i).join(path.sep);
    if (!exists(shortDir)) {
      fs.mkdirSync(shortDir);
    }
  }
};

var saveFilesFromBlobClient = function (blobClient) {
  var artifactNames = Object.keys(blobClient.artifacts);
  for (var i = artifactNames.length; i--; ) {
    blobClient.artifacts[artifactNames[i]].files.forEach(saveFile);
  }
};

/* * * * * * * Config Settings * * * * * * * */
var getConfig = function (startPath) {
  var root = getRootPath(startPath),
    config;

  if (!root) {
    return null;
  }

  config = fs.readFileSync(path.join(root, PROJECT_CONFIG));
  return JSON.parse(config);
};

var saveConfig = function (config) {
  var root = getRootPath();
  var configText = JSON.stringify(config, null, 2);
  fs.writeFileSync(path.join(root, PROJECT_CONFIG), configText);
};

var getAppName = function (startPath) {
  const root = getRootPath(startPath);
  return require(path.join(root, "package.json")).name;
};

var getPackageJSON = function (startPath) {
  var root = getRootPath(startPath);

  return JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf-8"));
};

var writePackageJSON = function (content, startPath) {
  var root = getRootPath(startPath);

  return fs.writeFileSync(
    path.join(root, "package.json"),
    JSON.stringify(content, null, 2)
  );
};

/**
 * Update the WebGME config based on the paths in the webgme-setup.json.
 *
 * @return {undefined}
 */
var updateWebGMEConfig = function (startPath) {
  var root = getRootPath(startPath),
    content = getWebGMEConfigContent(startPath),
    templatePath = path.join(__dirname, "res", "config.template.js.ejs"),
    template = _.template(fs.readFileSync(templatePath)),
    configPath = path.join(root, "config", "config.webgme.js");

  // Add webgme app name to the content
  var appName = require(path.join(root, "package.json")).name;
  content.appName = appName.replace(/-/g, "_");

  // Add default layout info
  var config = getConfig(startPath);
  if (config) {
    Object.keys(config).forEach((type) => {
      Object.keys(config[type].layouts || {}).forEach((layout) => {
        if (config[type].layouts[layout].enabled) {
          content.defaultLayout = layout;
        }
      });
    });
  }

  // Add router info
  content.routers = [];
  Object.keys(config).forEach((type) => {
    Object.keys(config[type].routers || {}).forEach((name) => {
      let router = config[type].routers[name];
      router.name = name;
      router.srcFile = `${router.src || router.path}/${name}.js`;
      content.routers.push(router);
    });
  });

  // Create the WebGME config file
  fs.writeFileSync(configPath, template(content));
};

/**
 * Get the paths from a config (sub) object such as "components" or
 * "dependencies"
 *
 * Input example: {
 *   plugins: {
 *      ...
 *   },
 *   seeds: {
 *      ...
 *   }
 * }
 *
 * @param {Object} config
 * @return {String[]}
 */
var getPathsFromConfigGroup = function (config) {
  return R.mapObj(function (componentType) {
    // ie, plugin/seed/etc OBJECT
    return R.values(componentType).map(function (component) {
      return component.src || component.path;
    });
  }, config);
};

var unique = function (array) {
  var duplicates = {};
  array.forEach(function (key) {
    duplicates[key] = 1;
  });
  return Object.keys(duplicates);
};

var getWebGMEConfigContent = function (startPath) {
  var arrays,
    config = getConfig(startPath),
    paths = {},
    categories = ["components", "dependencies"],
    configGroupPaths = categories.map(function (type) {
      return getPathsFromConfigGroup(config[type]);
    });

  // Merge the arrays for each componentType
  Object.keys(configGroupPaths[0]).forEach(function (type) {
    arrays = configGroupPaths.map(function (group) {
      // Remove duplicates
      return unique(group[type]);
    });

    paths[type] = arrays
      .reduce(R.concat) // Merge all paths
      .map(R.replace.bind(R, /\\/g, "/")); // Convert to use '/' for path separator
  });

  // Update visualizers to use the 'panel' entry (if applicable)
  if (config.components.visualizers) {
    // add the components
    let local = Object.keys(config.components.visualizers).map(
      (name) => config.components.visualizers[name].panel
    );

    // and the dependencies
    paths.visualizers = Object.keys(config.dependencies.visualizers)
      .map((name) =>
        [
          "node_modules",
          config.dependencies.visualizers[name].project,
          config.dependencies.visualizers[name].panel,
        ].join("/")
      )
      .concat(local);
  }

  // Set the requirejsPaths to be an array of all dependency paths
  paths.requirejsPaths = getRequireJSPaths(config, startPath);
  return paths;
};

var getRequireJSPaths = function (config, startPath) {
  var componentTypes = Object.keys(config.dependencies),
    components,
    paths = [],
    names,
    i;

  for (i = componentTypes.length; i--; ) {
    components = config.dependencies[componentTypes[i]];
    names = Object.keys(components);
    for (var j = names.length; j--; ) {
      paths.push({
        name: names[j],
        path: components[names[j]].src || components[names[j]].path,
      });
    }
  }

  // If it has any visualizers, add some boilerplate
  if (hasVisualizers(config)) {
    ["panels", "widgets"].forEach(function (name) {
      paths.push({
        name: name,
        path: "./src/visualizers/" + name,
      });
    });

    // Add all dependent visualizers
    // These are in the format 'widgets/DepViz': './node_modules/<project>/<path>'
    var vizConfig = config.dependencies.visualizers,
      dependentVizs = Object.keys(vizConfig),
      depName,
      depPath,
      project;

    for (i = dependentVizs.length; i--; ) {
      depName = vizConfig[dependentVizs[i]].src.split("/");
      depName.pop();
      depName = depName.join("/");

      project = vizConfig[dependentVizs[i]].project.toLowerCase();
      depPath = [
        ".",
        "node_modules",
        project,
        vizConfig[dependentVizs[i]].panel,
      ].join("/");

      paths.push({
        name: depName,
        path: depPath,
      });

      depPath = [
        ".",
        "node_modules",
        project,
        vizConfig[dependentVizs[i]].widget,
      ].join("/");

      paths.push({
        name: depName.replace("panel", "widget"),
        path: depPath,
      });
    }
  }

  // Add common directories for all the dependent projects (and ourself)
  let allProjects = componentTypes
    .map((type) => {
      let names = Object.keys(config.dependencies[type]);
      return names.map((name) => config.dependencies[type][name].project);
    })
    .reduce((l1, l2) => l1.concat(l2), []);
  let projects = _.uniq(allProjects);
  projects.forEach((project) => {
    project = project.toLowerCase();
    paths.push({
      name: project,
      path: `./node_modules/${project}/src/common`,
    });
  });
  paths.push({
    name: getAppName(startPath),
    path: "./src/common",
  });

  return paths;
};

/**
 * Check that the config contains at least one visualizer (in either components
 * or dependencies)
 *
 * @param config
 * @return {Boolean}
 */
var hasVisualizers = function (config) {
  return ["components", "dependencies"].reduce(function (prev, type) {
    return (
      prev ||
      (config[type].visualizers &&
        Object.keys(config[type].visualizers).length > 0)
    );
  }, false);
};

var getConfigPath = function (project) {
  return path.join(getRootPath(), "node_modules", project, PROJECT_CONFIG);
};

/**
 * Get the GME path for the given dependent project or the working project
 * if unspecified
 *
 * @param {String} project
 * @return {String} path
 */
var getGMEConfigPath = function (project) {
  var gmeConfigPath,
    projectPath = "";

  if (project) {
    projectPath = path.join("node_modules", project);
  }
  gmeConfigPath = path.join(getRootPath(), projectPath, "config");

  return gmeConfigPath;
};

/**
 * Find the first path containing the given item.
 *
 * @param {String[]} pathType
 * @param {String} item
 * @return {String} path containing the item
 */
var getPathContaining = function (paths, item) {
  var validPaths = paths.filter(function (p) {
    return (
      exists(p) &&
      fs.readdirSync(p).indexOf(item) +
        fs.readdirSync(p).indexOf(item + ".js") !==
        -2
    );
  });
  return validPaths.length ? validPaths[0] : null;
};

/**
 * Get the name of the package installed with "npmPackage"
 *
 * @param {String} npmPackage
 * @return {String} name
 */
var getPackageName = function (npmPackage) {
  // FIXME: It currently assumes everything is a github url. Should support
  // hashes, packages, etc
  // Ideally, we could use an npm feature to do this
  if (npmPackage[0] === ".") {
    // File path
    return npmPackage.split(path.sep).pop();
  }

  // Github url: project/repo
  return npmPackage.split("/").pop().replace(/#.*$/, "");
};

var loadPaths = function (requirejs) {
  const webgmeEngineRoot = path.dirname(require.resolve("webgme-engine"));
  requirejs.config({
    nodeRequire: require,
    baseUrl: __dirname,
    paths: {
      text: `${webgmeEngineRoot}/src/common/lib/requirejs/text`,
      coreplugins: `${webgmeEngineRoot}/src/plugin/coreplugins`,
      plugin: `${webgmeEngineRoot}/src/plugin`,
      common: `${webgmeEngineRoot}/src/common`,

      "plugin/PluginGenerator/PluginGenerator": `${webgmeEngineRoot}/src/plugin/coreplugins/PluginGenerator/`,
      "plugin/AddOnGenerator/AddOnGenerator": `${webgmeEngineRoot}/src/plugin/coreplugins/AddOnGenerator/`,
      "plugin/DecoratorGenerator/DecoratorGenerator": `${webgmeEngineRoot}/src/plugin/coreplugins/DecoratorGenerator/`,
      "plugin/LayoutGenerator/LayoutGenerator": `${webgmeEngineRoot}/src/plugin/coreplugins/LayoutGenerator/`,
      "plugin/VisualizerGenerator/VisualizerGenerator": `${webgmeEngineRoot}/src/plugin/coreplugins/VisualizerGenerator/`,
    },
  });
};

var normalizePath = function (dirs) {
  if (path.sep === "\\") {
    return dirs.replace(/\\/g, "/");
  }
  return dirs;
};

const installProject = function (projectName, isDev, callback) {
  let projectRoot = getRootPath();
  let cmd = isDev
    ? `npm install ${projectName} --save-dev`
    : `npm install ${projectName} --save`;
  let job = spawn(cmd, { cwd: projectRoot });

  logger.info(cmd);
  logger.writeStream(job.stdout);
  logger.errorStream(job.stderr);

  job.on("close", (code) => {
    logger.info(`npm exited with: ${code}`);
    if (code === 0) {
      // Success!
      return callback(null);
    } else {
      let err = `Could not find project (${projectName})!`;
      logger.error(err);
      return callback(err);
    }
  });
};

module.exports = {
  PROJECT_CONFIG: PROJECT_CONFIG,
  saveConfig: saveConfig,
  getConfig: getConfig,
  getRootPath: getRootPath,
  getGMEConfigPath: getGMEConfigPath,
  getPathContaining: getPathContaining,
  getConfigPath: getConfigPath,
  updateWebGMEConfig: updateWebGMEConfig,
  saveFilesFromBlobClient: saveFilesFromBlobClient,
  saveFile: saveFile,
  loadPaths: loadPaths,
  getPackageName: getPackageName,
  normalize: normalizePath,
  installProject: installProject,
  mkdir: createDir,
  getPackageJSON: getPackageJSON,
  writePackageJSON: writePackageJSON,
  changeToRootDir: changeToRootDir,
};