ScriptFileStorage.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. var fs = require('fs');
  2. var path = require('path');
  3. var async = require('async');
  4. var glob = require('glob');
  5. var MODULE_HEADER = '(function (exports, require, module, __filename, __dirname) { ';
  6. var MODULE_TRAILER = '\n});';
  7. var MODULE_WRAP_REGEX = new RegExp(
  8. '^' + escapeRegex(MODULE_HEADER) +
  9. '([\\s\\S]*)' +
  10. escapeRegex(MODULE_TRAILER) + '$'
  11. );
  12. var CONVENTIONAL_DIRS_PATTERN = '{*.js,lib/**/*.js,node_modules/**/*.js,test/**/*.js}';
  13. var ALL_JS_PATTERN = '**/*.js';
  14. function escapeRegex(str) {
  15. return str.replace(/([/\\.?*()^${}|[\]])/g, '\\$1');
  16. }
  17. /**
  18. * @param {{preload}} config
  19. * @param {ScriptManager} scriptManager
  20. * @constructor
  21. */
  22. function ScriptFileStorage(config, scriptManager) {
  23. config = config || {};
  24. this._scriptManager = scriptManager;
  25. this._noPreload = config.preload === false;
  26. }
  27. var $class = ScriptFileStorage.prototype;
  28. $class.save = function(path, content, callback) {
  29. var match = MODULE_WRAP_REGEX.exec(content);
  30. if (!match) {
  31. callback(new Error('The new content is not a valid node.js script.'));
  32. return;
  33. }
  34. var newSource = match[1];
  35. async.waterfall([
  36. fs.readFile.bind(fs, path, 'utf-8'),
  37. function(oldContent, cb) {
  38. var match = /^(\#\!.*)/.exec(oldContent);
  39. if (match)
  40. newSource = match[1] + newSource;
  41. fs.writeFile(path, newSource, cb);
  42. }
  43. ],
  44. callback);
  45. };
  46. /**
  47. * @param {string} path
  48. * @param {function(Object, string)} callback
  49. */
  50. $class.load = function(path, callback) {
  51. fs.readFile(
  52. path,
  53. 'utf-8',
  54. function(err, content) {
  55. if (err) return callback(err);
  56. // remove shebang
  57. content = content.replace(/^\#\!.*/, '');
  58. var source = MODULE_HEADER + content + MODULE_TRAILER;
  59. return callback(null, source);
  60. }
  61. );
  62. };
  63. /**
  64. * @param {string} mainScriptFile
  65. * @param {function(Object, string)} callback
  66. * @this {ScriptFileStorage}
  67. */
  68. $class.findApplicationRoot = function(mainScriptFile, callback) {
  69. fs.realpath(mainScriptFile, function(err, realPath) {
  70. if (err) {
  71. console.log('Cannot resolve real path of %s: %s', mainScriptFile, err);
  72. realPath = mainScriptFile;
  73. }
  74. this._findApplicationRootForRealFile(realPath, callback);
  75. }.bind(this));
  76. };
  77. /**
  78. * For a given script file, find the root directory containing all application
  79. * source files.
  80. *
  81. * Example:
  82. * file = ~/work/app/bin/cli.js
  83. * root = ~/work/app
  84. *
  85. * The algorithm:
  86. *
  87. * By default, we assume that the source file is in the root directory
  88. * (~/work/app/bin in the example above).
  89. *
  90. * If this directory does not contain 'package.json' and the parent directory
  91. * contains 'package.json', then we assume the parent directory is
  92. * the application root (~/work/app in the example above).
  93. *
  94. * @param {string} file
  95. * @param {function(Object, string)} callback
  96. * @this {ScriptFileStorage}
  97. */
  98. $class._findApplicationRootForRealFile = function(file, callback) {
  99. var mainDir = path.dirname(file);
  100. var parentDir = path.dirname(mainDir);
  101. async.detect(
  102. [mainDir, parentDir],
  103. this._isApplicationRoot.bind(this),
  104. function(result) {
  105. callback(null, result || mainDir, !!result);
  106. }
  107. );
  108. };
  109. /**
  110. * @param {string} folder
  111. * @param {function(boolean)} callback
  112. */
  113. $class._isApplicationRoot = function(folder, callback) {
  114. fs.exists(path.join(folder, 'package.json'), callback);
  115. };
  116. /**
  117. * @param {string} rootFolder
  118. * @param {string} pattern
  119. * @param {function(Object, Array.<string>?)} callback
  120. */
  121. $class.listScripts = function(rootFolder, pattern, callback) {
  122. var last = this._lastList;
  123. if (last && last.rootFolder == rootFolder && last.pattern == pattern) {
  124. process.nextTick(function() { callback(last.error, last.result); });
  125. return;
  126. }
  127. // This simpler solution unfortunately does not work on windows
  128. // see https://github.com/isaacs/node-glob/pull/68
  129. // glob(
  130. // '**/*.js',
  131. // { root: rootFolder },
  132. // callback
  133. // );
  134. glob(
  135. pattern,
  136. {
  137. cwd: rootFolder,
  138. strict: false
  139. },
  140. function(err, result) {
  141. if (result) {
  142. result = result.map(function(relativeUnixPath) {
  143. var relativePath = relativeUnixPath.split('/').join(path.sep);
  144. return path.join(rootFolder, relativePath);
  145. });
  146. }
  147. this._lastList = {
  148. rootFolder: rootFolder,
  149. pattern: pattern,
  150. error: err,
  151. result: result
  152. };
  153. callback(err, result);
  154. }.bind(this)
  155. );
  156. };
  157. $class._findScriptsOfRunningApp = function(mainScriptFile, callback) {
  158. if (!mainScriptFile) {
  159. // mainScriptFile is null when running in the REPL mode
  160. return process.nextTick(callback.bind(null, null, []));
  161. }
  162. async.waterfall(
  163. [
  164. this.findApplicationRoot.bind(this, mainScriptFile),
  165. function(dir, isRoot, cb) {
  166. var pattern = isRoot ? ALL_JS_PATTERN : CONVENTIONAL_DIRS_PATTERN;
  167. this.listScripts(dir, pattern, cb);
  168. }.bind(this)
  169. ],
  170. callback
  171. );
  172. };
  173. $class._findScriptsOfStartDirectoryApp = function(startDirectory, callback) {
  174. this._isApplicationRoot(
  175. startDirectory,
  176. function handleIsStartDirectoryApplicationRoot(result) {
  177. if (!result) {
  178. callback(null, []);
  179. } else {
  180. this.listScripts(startDirectory, ALL_JS_PATTERN, callback);
  181. }
  182. }.bind(this)
  183. );
  184. };
  185. /**
  186. * @param {string} startDirectory
  187. * @param {string} mainScriptFile
  188. * @param {function(Object, Array.<string>)} callback
  189. * @this {ScriptFileStorage}
  190. */
  191. $class.findAllApplicationScripts = function(startDirectory, mainScriptFile, callback) {
  192. if (this._noPreload) {
  193. return process.nextTick(function() { callback(null, []); });
  194. }
  195. async.series(
  196. [
  197. this._findScriptsOfRunningApp.bind(this, mainScriptFile),
  198. this._findScriptsOfStartDirectoryApp.bind(this, startDirectory)
  199. ],
  200. function(err, results) {
  201. if (err) return callback(err);
  202. var files = results[0].concat(results[1]);
  203. // filter out duplicates and files to hide
  204. files = files.filter(function(elem, ix, arr) {
  205. return arr.indexOf(elem) >= ix && !this._scriptManager.isScriptHidden(elem);
  206. }.bind(this));
  207. return callback(null, files);
  208. }.bind(this)
  209. );
  210. };
  211. exports.ScriptFileStorage = ScriptFileStorage;