commoner.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. var assert = require("assert");
  2. var path = require("path");
  3. var fs = require("fs");
  4. var Q = require("q");
  5. var iconv = require("iconv-lite");
  6. var ReadFileCache = require("./cache").ReadFileCache;
  7. var Watcher = require("./watcher").Watcher;
  8. var contextModule = require("./context");
  9. var BuildContext = contextModule.BuildContext;
  10. var PreferredFileExtension = contextModule.PreferredFileExtension;
  11. var ModuleReader = require("./reader").ModuleReader;
  12. var output = require("./output");
  13. var DirOutput = output.DirOutput;
  14. var StdOutput = output.StdOutput;
  15. var util = require("./util");
  16. var log = util.log;
  17. var Ap = Array.prototype;
  18. var each = Ap.forEach;
  19. // Better stack traces for promises.
  20. Q.longStackSupport = true;
  21. function Commoner() {
  22. var self = this;
  23. assert.ok(self instanceof Commoner);
  24. Object.defineProperties(self, {
  25. customVersion: { value: null, writable: true },
  26. customOptions: { value: [] },
  27. resolvers: { value: [] },
  28. processors: { value: [] }
  29. });
  30. }
  31. var Cp = Commoner.prototype;
  32. Cp.version = function(version) {
  33. this.customVersion = version;
  34. return this; // For chaining.
  35. };
  36. // Add custom command line options
  37. Cp.option = function() {
  38. this.customOptions.push(Ap.slice.call(arguments));
  39. return this; // For chaining.
  40. };
  41. // A resolver is a function that takes a module identifier and returns
  42. // the unmodified source of the corresponding module, either as a string
  43. // or as a promise for a string.
  44. Cp.resolve = function() {
  45. each.call(arguments, function(resolver) {
  46. assert.strictEqual(typeof resolver, "function");
  47. this.resolvers.push(resolver);
  48. }, this);
  49. return this; // For chaining.
  50. };
  51. // A processor is a function that takes a module identifier and a string
  52. // representing the source of the module and returns a modified version of
  53. // the source, either as a string or as a promise for a string.
  54. Cp.process = function(processor) {
  55. each.call(arguments, function(processor) {
  56. assert.strictEqual(typeof processor, "function");
  57. this.processors.push(processor);
  58. }, this);
  59. return this; // For chaining.
  60. };
  61. Cp.buildP = function(options, roots) {
  62. var self = this;
  63. var sourceDir = options.sourceDir;
  64. var outputDir = options.outputDir;
  65. var readFileCache = new ReadFileCache(sourceDir, options.sourceCharset);
  66. var waiting = 0;
  67. var output = outputDir
  68. ? new DirOutput(outputDir)
  69. : new StdOutput;
  70. if (self.watch) {
  71. new Watcher(readFileCache).on("changed", function(file) {
  72. log.err(file + " changed; rebuilding...", "yellow");
  73. rebuild();
  74. });
  75. }
  76. function outputModules(modules) {
  77. // Note that output.outputModules comes pre-bound.
  78. modules.forEach(output.outputModule);
  79. return modules;
  80. }
  81. function finish(result) {
  82. rebuild.ing = false;
  83. if (waiting > 0) {
  84. waiting = 0;
  85. process.nextTick(rebuild);
  86. }
  87. return result;
  88. }
  89. function rebuild() {
  90. if (rebuild.ing) {
  91. waiting += 1;
  92. return;
  93. }
  94. rebuild.ing = true;
  95. var context = new BuildContext(options, readFileCache);
  96. if (self.preferredFileExtension)
  97. context.setPreferredFileExtension(
  98. self.preferredFileExtension);
  99. context.setCacheDirectory(self.cacheDir);
  100. context.setIgnoreDependencies(self.ignoreDependencies);
  101. context.setRelativize(self.relativize);
  102. context.setUseProvidesModule(self.useProvidesModule);
  103. return new ModuleReader(
  104. context,
  105. self.resolvers,
  106. self.processors
  107. ).readMultiP(context.expandIdsOrGlobsP(roots))
  108. .then(context.ignoreDependencies ? pass : collectDepsP)
  109. .then(outputModules)
  110. .then(outputDir ? printModuleIds : pass)
  111. .then(finish, function(err) {
  112. log.err(err.stack);
  113. if (!self.watch) {
  114. // If we're not building with --watch, throw the error
  115. // so that cliBuildP can call process.exit(-1).
  116. throw err;
  117. }
  118. finish();
  119. });
  120. }
  121. return (
  122. // If outputDir is falsy, we can't (and don't need to) mkdirP it.
  123. outputDir ? util.mkdirP : Q
  124. )(outputDir).then(rebuild);
  125. };
  126. function pass(modules) {
  127. return modules;
  128. }
  129. function collectDepsP(rootModules) {
  130. var modules = [];
  131. var seenIds = {};
  132. function traverse(module) {
  133. if (seenIds.hasOwnProperty(module.id))
  134. return Q(modules);
  135. seenIds[module.id] = true;
  136. return module.getRequiredP().then(function(reqs) {
  137. return Q.all(reqs.map(traverse));
  138. }).then(function() {
  139. modules.push(module);
  140. return modules;
  141. });
  142. }
  143. return Q.all(rootModules.map(traverse)).then(
  144. function() { return modules });
  145. }
  146. function printModuleIds(modules) {
  147. log.out(JSON.stringify(modules.map(function(module) {
  148. return module.id;
  149. })));
  150. return modules;
  151. }
  152. Cp.forceResolve = function(forceId, source) {
  153. this.resolvers.unshift(function(id) {
  154. if (id === forceId)
  155. return source;
  156. });
  157. };
  158. Cp.cliBuildP = function() {
  159. var version = this.customVersion || require("../package.json").version;
  160. return Q.spread([this, version], cliBuildP);
  161. };
  162. function cliBuildP(commoner, version) {
  163. var options = require("commander");
  164. var workingDir = process.cwd();
  165. var sourceDir = workingDir;
  166. var outputDir = null;
  167. var roots;
  168. options.version(version)
  169. .usage("[options] <source directory> <output directory> [<module ID> [<module ID> ...]]")
  170. .option("-c, --config [file]", "JSON configuration file (no file or - means STDIN)")
  171. .option("-w, --watch", "Continually rebuild")
  172. .option("-x, --extension <js | coffee | ...>",
  173. "File extension to assume when resolving module identifiers")
  174. .option("--relativize", "Rewrite all module identifiers to be relative")
  175. .option("--follow-requires", "Scan modules for required dependencies")
  176. .option("--use-provides-module", "Respect @providesModules pragma in files")
  177. .option("--cache-dir <directory>", "Alternate directory to use for disk cache")
  178. .option("--no-cache-dir", "Disable the disk cache")
  179. .option("--source-charset <utf8 | win1252 | ...>",
  180. "Charset of source (default: utf8)")
  181. .option("--output-charset <utf8 | win1252 | ...>",
  182. "Charset of output (default: utf8)");
  183. commoner.customOptions.forEach(function(customOption) {
  184. options.option.apply(options, customOption);
  185. });
  186. options.parse(process.argv.slice(0));
  187. var pfe = new PreferredFileExtension(options.extension || "js");
  188. // TODO Decide whether passing options to buildP via instance
  189. // variables is preferable to passing them as arguments.
  190. commoner.preferredFileExtension = pfe;
  191. commoner.watch = options.watch;
  192. commoner.ignoreDependencies = !options.followRequires;
  193. commoner.relativize = options.relativize;
  194. commoner.useProvidesModule = options.useProvidesModule;
  195. commoner.sourceCharset = normalizeCharset(options.sourceCharset);
  196. commoner.outputCharset = normalizeCharset(options.outputCharset);
  197. function fileToId(file) {
  198. file = absolutePath(workingDir, file);
  199. assert.ok(fs.statSync(file).isFile(), file);
  200. return pfe.trim(path.relative(sourceDir, file));
  201. }
  202. var args = options.args.slice(0);
  203. var argc = args.length;
  204. if (argc === 0) {
  205. if (options.config === true) {
  206. log.err("Cannot read --config from STDIN when reading " +
  207. "source from STDIN");
  208. process.exit(-1);
  209. }
  210. sourceDir = workingDir;
  211. outputDir = null;
  212. roots = ["<stdin>"];
  213. commoner.forceResolve("<stdin>", util.readFromStdinP());
  214. // Ignore dependencies because we wouldn't know how to find them.
  215. commoner.ignoreDependencies = true;
  216. } else {
  217. var first = absolutePath(workingDir, args[0]);
  218. var stats = fs.statSync(first);
  219. if (argc === 1) {
  220. var firstId = fileToId(first);
  221. sourceDir = workingDir;
  222. outputDir = null;
  223. roots = [firstId];
  224. commoner.forceResolve(
  225. firstId,
  226. util.readFileP(first, commoner.sourceCharset)
  227. );
  228. // Ignore dependencies because we wouldn't know how to find them.
  229. commoner.ignoreDependencies = true;
  230. } else if (stats.isDirectory(first)) {
  231. sourceDir = first;
  232. outputDir = absolutePath(workingDir, args[1]);
  233. roots = args.slice(2);
  234. if (roots.length === 0)
  235. roots.push(commoner.preferredFileExtension.glob());
  236. } else {
  237. options.help();
  238. process.exit(-1);
  239. }
  240. }
  241. commoner.cacheDir = null;
  242. if (options.cacheDir === false) {
  243. // Received the --no-cache-dir option, so disable the disk cache.
  244. } else if (typeof options.cacheDir === "string") {
  245. commoner.cacheDir = absolutePath(workingDir, options.cacheDir);
  246. } else if (outputDir) {
  247. // The default cache directory lives inside the output directory.
  248. commoner.cacheDir = path.join(outputDir, ".module-cache");
  249. }
  250. var promise = getConfigP(
  251. workingDir,
  252. options.config
  253. ).then(function(config) {
  254. var cleanOptions = {};
  255. options.options.forEach(function(option) {
  256. var name = util.camelize(option.name());
  257. if (options.hasOwnProperty(name)) {
  258. cleanOptions[name] = options[name];
  259. }
  260. });
  261. cleanOptions.version = version;
  262. cleanOptions.config = config;
  263. cleanOptions.sourceDir = sourceDir;
  264. cleanOptions.outputDir = outputDir;
  265. cleanOptions.sourceCharset = commoner.sourceCharset;
  266. cleanOptions.outputCharset = commoner.outputCharset;
  267. return commoner.buildP(cleanOptions, roots);
  268. });
  269. if (!commoner.watch) {
  270. // If we're building from the command line without --watch, any
  271. // build errors should immediately terminate the process with a
  272. // non-zero error code.
  273. promise = promise.catch(function(err) {
  274. log.err(err.stack);
  275. process.exit(-1);
  276. });
  277. }
  278. return promise;
  279. }
  280. function normalizeCharset(charset) {
  281. charset = charset
  282. && charset.replace(/[- ]/g, "").toLowerCase()
  283. || "utf8";
  284. assert.ok(
  285. iconv.encodingExists(charset),
  286. "Unrecognized charset: " + charset
  287. );
  288. return charset;
  289. }
  290. function absolutePath(workingDir, pathToJoin) {
  291. if (pathToJoin) {
  292. workingDir = path.normalize(workingDir);
  293. pathToJoin = path.normalize(pathToJoin);
  294. // TODO: use path.isAbsolute when Node < 0.10 is unsupported
  295. if (path.resolve(pathToJoin) !== pathToJoin) {
  296. pathToJoin = path.join(workingDir, pathToJoin);
  297. }
  298. }
  299. return pathToJoin;
  300. }
  301. function getConfigP(workingDir, configFile) {
  302. if (typeof configFile === "undefined")
  303. return Q({}); // Empty config.
  304. if (configFile === true || // --config is present but has no argument
  305. configFile === "<stdin>" ||
  306. configFile === "-" ||
  307. configFile === path.sep + path.join("dev", "stdin")) {
  308. return util.readJsonFromStdinP(
  309. 1000, // Time limit in milliseconds before warning displayed.
  310. "Expecting configuration from STDIN (pass --config <file> " +
  311. "if stuck here)...",
  312. "yellow"
  313. );
  314. }
  315. return util.readJsonFileP(absolutePath(workingDir, configFile));
  316. }
  317. exports.Commoner = Commoner;