exec.js 6.7 KB
Newer Older
jatuporn Tonggasem's avatar
jatuporn Tonggasem committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
const path = require('path');
const fs = require('fs');
const existsSync = fs.existsSync;
const utils = require('../utils');

module.exports = exec;
module.exports.expandScript = expandScript;

/**
 * Reads the cwd/package.json file and looks to see if it can load a script
 * and possibly an exec first from package.main, then package.start.
 *
 * @return {Object} exec & script if found
 */
function execFromPackage() {
  // doing a try/catch because we can't use the path.exist callback pattern
  // or we could, but the code would get messy, so this will do exactly
  // what we're after - if the file doesn't exist, it'll throw.
  try {
    // note: this isn't nodemon's package, it's the user's cwd package
    var pkg = require(path.join(process.cwd(), 'package.json'));
    if (pkg.main !== undefined) {
      // no app found to run - so give them a tip and get the feck out
      return { exec: null, script: pkg.main };
    }

    if (pkg.scripts && pkg.scripts.start) {
      return { exec: pkg.scripts.start };
    }
  } catch (e) { }

  return null;
}

function replace(map, str) {
  var re = new RegExp('{{(' + Object.keys(map).join('|') + ')}}', 'g');
  return str.replace(re, function (all, m) {
    return map[m] || all || '';
  });
}

function expandScript(script, ext) {
  if (!ext) {
    ext = '.js';
  }
  if (script.indexOf(ext) !== -1) {
    return script;
  }

  if (existsSync(path.resolve(script))) {
    return script;
  }

  if (existsSync(path.resolve(script + ext))) {
    return script + ext;
  }

  return script;
}

/**
 * Discovers all the options required to run the script
 * and if a custom exec has been passed in, then it will
 * also try to work out what extensions to monitor and
 * whether there's a special way of running that script.
 *
 * @param  {Object} nodemonOptions
 * @param  {Object} execMap
 * @return {Object} new and updated version of nodemonOptions
 */
function exec(nodemonOptions, execMap) {
  if (!execMap) {
    execMap = {};
  }

  var options = utils.clone(nodemonOptions || {});
  var script;

  // if there's no script passed, try to get it from the first argument
  if (!options.script && (options.args || []).length) {
    script = expandScript(options.args[0],
      options.ext && ('.' + (options.ext || 'js').split(',')[0]));

    // if the script was found, shift it off our args
    if (script !== options.args[0]) {
      options.script = script;
      options.args.shift();
    }
  }

  // if there's no exec found yet, then try to read it from the local
  // package.json this logic used to sit in the cli/parse, but actually the cli
  // should be parsed first, then the user options (via nodemon.json) then
  // finally default down to pot shots at the directory via package.json
  if (!options.exec && !options.script) {
    var found = execFromPackage();
    if (found !== null) {
      if (found.exec) {
        options.exec = found.exec;
      }
      if (!options.script) {
        options.script = found.script;
      }
      if (Array.isArray(options.args) &&
        options.scriptPosition === null) {
        options.scriptPosition = options.args.length;
      }
    }
  }

  // var options = utils.clone(nodemonOptions || {});
  script = path.basename(options.script || '');

  var scriptExt = path.extname(script).slice(1);

  var extension = options.ext;
  if (extension === undefined) {
    var isJS = scriptExt === 'js' || scriptExt === 'mjs';
    extension = (isJS || !scriptExt) ? 'js,mjs' : scriptExt;
    extension += ',json'; // Always watch JSON files
  }

  var execDefined = !!options.exec;

  // allows the user to simplify cli usage:
  // https://github.com/remy/nodemon/issues/195
  // but always give preference to the user defined argument
  if (!options.exec && execMap[scriptExt] !== undefined) {
    options.exec = execMap[scriptExt];
    execDefined = true;
  }

  options.execArgs = nodemonOptions.execArgs || [];

  if (Array.isArray(options.exec)) {
    options.execArgs = options.exec;
    options.exec = options.execArgs.shift();
  }

  if (options.exec === undefined) {
    options.exec = 'node';
  } else {
    // allow variable substitution for {{filename}} and {{pwd}}
    var substitution = replace.bind(null, {
      filename: options.script,
      pwd: process.cwd(),
    });

    var newExec = substitution(options.exec);
    if (newExec !== options.exec &&
      options.exec.indexOf('{{filename}}') !== -1) {
      options.script = null;
    }
    options.exec = newExec;

    var newExecArgs = options.execArgs.map(substitution);
    if (newExecArgs.join('') !== options.execArgs.join('')) {
      options.execArgs = newExecArgs;
      delete options.script;
    }
  }


  if (options.exec === 'node' && options.nodeArgs && options.nodeArgs.length) {
    options.execArgs = options.execArgs.concat(options.nodeArgs);
  }

  // note: indexOf('coffee') handles both .coffee and .litcoffee
  if (!execDefined && options.exec === 'node' &&
    scriptExt.indexOf('coffee') !== -1) {
    options.exec = 'coffee';

    // we need to get execArgs set before the script
    // for example, in `nodemon --debug my-script.coffee --my-flag`, debug is an
    // execArg, while my-flag is a script arg
    var leadingArgs = (options.args || []).splice(0, options.scriptPosition);
    options.execArgs = options.execArgs.concat(leadingArgs);
    options.scriptPosition = 0;

    if (options.execArgs.length > 0) {
      // because this is the coffee executable, we need to combine the exec args
      // into a single argument after the nodejs flag
      options.execArgs = ['--nodejs', options.execArgs.join(' ')];
    }
  }

  if (options.exec === 'coffee') {
    // don't override user specified extension tracking
    if (options.ext === undefined) {
      if (extension) { extension += ','; }
      extension += 'coffee,litcoffee';
    }

    // because windows can't find 'coffee', it needs the real file 'coffee.cmd'
    if (utils.isWindows) {
      options.exec += '.cmd';
    }
  }

  // allow users to make a mistake on the extension to monitor
  // converts .js, jade => js,jade
  // BIG NOTE: user can't do this: nodemon -e *.js
  // because the terminal will automatically expand the glob against
  // the file system :(
  extension = (extension.match(/[^,*\s]+/g) || [])
    .map(ext => ext.replace(/^\./, ''))
    .join(',');

  options.ext = extension;

  if (options.script) {
    options.script = expandScript(options.script,
      extension && ('.' + extension.split(',')[0]));
  }

  options.env = {};
  // make sure it's an object (and since we don't have )
  if (({}).toString.apply(nodemonOptions.env) === '[object Object]') {
    options.env = utils.clone(nodemonOptions.env);
  } else if (nodemonOptions.env !== undefined) {
    throw new Error('nodemon env values must be an object: { PORT: 8000 }');
  }

  return options;
}