reader.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  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 createHash = require("crypto").createHash;
  7. var detective = require("detective");
  8. var util = require("./util");
  9. var BuildContext = require("./context").BuildContext;
  10. var slice = Array.prototype.slice;
  11. function getRequiredIDs(id, source) {
  12. var ids = {};
  13. detective(source).forEach(function (dep) {
  14. ids[path.normalize(path.join(id, "..", dep))] = true;
  15. });
  16. return Object.keys(ids);
  17. }
  18. function ModuleReader(context, resolvers, processors) {
  19. var self = this;
  20. assert.ok(self instanceof ModuleReader);
  21. assert.ok(context instanceof BuildContext);
  22. assert.ok(resolvers instanceof Array);
  23. assert.ok(processors instanceof Array);
  24. var hash = createHash("sha1").update(context.optionsHash + "\0");
  25. function hashCallbacks(salt) {
  26. hash.update(salt + "\0");
  27. var cbs = util.flatten(slice.call(arguments, 1));
  28. cbs.forEach(function(cb) {
  29. assert.strictEqual(typeof cb, "function");
  30. hash.update(cb + "\0");
  31. });
  32. return cbs;
  33. }
  34. resolvers = hashCallbacks("resolvers", resolvers, warnMissingModule);
  35. var procArgs = [processors];
  36. if (context.relativize && !context.ignoreDependencies)
  37. procArgs.push(require("./relative").getProcessor(self));
  38. processors = hashCallbacks("processors", procArgs);
  39. Object.defineProperties(self, {
  40. context: { value: context },
  41. idToHash: { value: {} },
  42. resolvers: { value: resolvers },
  43. processors: { value: processors },
  44. salt: { value: hash.digest("hex") }
  45. });
  46. }
  47. ModuleReader.prototype = {
  48. getSourceP: util.cachedMethod(function(id) {
  49. var context = this.context;
  50. var copy = this.resolvers.slice(0).reverse();
  51. assert.ok(copy.length > 0, "no source resolvers registered");
  52. function tryNextResolverP() {
  53. var resolve = copy.pop();
  54. try {
  55. var promise = Q(resolve && resolve.call(context, id));
  56. } catch (e) {
  57. promise = Q.reject(e);
  58. }
  59. return resolve ? promise.then(function(result) {
  60. if (typeof result === "string")
  61. return result;
  62. return tryNextResolverP();
  63. }, tryNextResolverP) : promise;
  64. }
  65. return tryNextResolverP();
  66. }),
  67. getCanonicalIdP: util.cachedMethod(function(id) {
  68. var reader = this;
  69. if (reader.context.useProvidesModule) {
  70. return reader.getSourceP(id).then(function(source) {
  71. return reader.context.getProvidedId(source) || id;
  72. });
  73. } else {
  74. return Q(id);
  75. }
  76. }),
  77. readModuleP: util.cachedMethod(function(id) {
  78. var reader = this;
  79. return reader.getSourceP(id).then(function(source) {
  80. if (reader.context.useProvidesModule) {
  81. // If the source contains a @providesModule declaration, treat
  82. // that declaration as canonical. Note that the Module object
  83. // returned by readModuleP might have an .id property whose
  84. // value differs from the original id parameter.
  85. id = reader.context.getProvidedId(source) || id;
  86. }
  87. assert.strictEqual(typeof source, "string");
  88. var hash = createHash("sha1")
  89. .update("module\0")
  90. .update(id + "\0")
  91. .update(reader.salt + "\0")
  92. .update(source.length + "\0" + source)
  93. .digest("hex");
  94. if (reader.idToHash.hasOwnProperty(id)) {
  95. // Ensure that the same module identifier is not
  96. // provided by distinct modules.
  97. assert.strictEqual(
  98. reader.idToHash[id], hash,
  99. "more than one module named " +
  100. JSON.stringify(id));
  101. } else {
  102. reader.idToHash[id] = hash;
  103. }
  104. return reader.buildModuleP(id, hash, source);
  105. });
  106. }),
  107. buildModuleP: util.cachedMethod(function(id, hash, source) {
  108. var reader = this;
  109. return reader.processOutputP(
  110. id, hash, source
  111. ).then(function(output) {
  112. return new Module(reader, id, hash, output);
  113. });
  114. }, function(id, hash, source) {
  115. return hash;
  116. }),
  117. processOutputP: function(id, hash, source) {
  118. var reader = this;
  119. var cacheDir = reader.context.cacheDir;
  120. var manifestDir = cacheDir && path.join(cacheDir, "manifest");
  121. var charset = reader.context.options.outputCharset;
  122. function buildP() {
  123. var promise = Q(source);
  124. reader.processors.forEach(function(build) {
  125. promise = promise.then(function(input) {
  126. return util.waitForValuesP(
  127. build.call(reader.context, id, input)
  128. );
  129. });
  130. });
  131. return promise.then(function(output) {
  132. if (typeof output === "string") {
  133. output = { ".js": output };
  134. } else {
  135. assert.strictEqual(typeof output, "object");
  136. }
  137. return util.waitForValuesP(output);
  138. }).then(function(output) {
  139. util.log.err(
  140. "built Module(" + JSON.stringify(id) + ")",
  141. "cyan"
  142. );
  143. return output;
  144. }).catch(function(err) {
  145. // Provide additional context for uncaught build errors.
  146. util.log.err("Error while reading module " + id + ":");
  147. throw err;
  148. });
  149. }
  150. if (manifestDir) {
  151. return util.mkdirP(manifestDir).then(function(manifestDir) {
  152. var manifestFile = path.join(manifestDir, hash + ".json");
  153. return util.readJsonFileP(manifestFile).then(function(manifest) {
  154. Object.keys(manifest).forEach(function(key) {
  155. var cacheFile = path.join(cacheDir, manifest[key]);
  156. manifest[key] = util.readFileP(cacheFile);
  157. });
  158. return util.waitForValuesP(manifest, true);
  159. }).catch(function(err) {
  160. return buildP().then(function(output) {
  161. var manifest = {};
  162. Object.keys(output).forEach(function(key) {
  163. var cacheFile = manifest[key] = hash + key;
  164. var fullPath = path.join(cacheDir, cacheFile);
  165. if (charset) {
  166. fs.writeFileSync(fullPath, iconv.encode(output[key], charset))
  167. } else {
  168. fs.writeFileSync(fullPath, output[key], "utf8");
  169. }
  170. });
  171. fs.writeFileSync(
  172. manifestFile,
  173. JSON.stringify(manifest),
  174. "utf8"
  175. );
  176. return output;
  177. });
  178. });
  179. });
  180. }
  181. return buildP();
  182. },
  183. readMultiP: function(ids) {
  184. var reader = this;
  185. return Q(ids).all().then(function(ids) {
  186. if (ids.length === 0)
  187. return ids; // Shortcut.
  188. var modulePs = ids.map(reader.readModuleP, reader);
  189. return Q(modulePs).all().then(function(modules) {
  190. var seen = {};
  191. var result = [];
  192. modules.forEach(function(module) {
  193. if (!seen.hasOwnProperty(module.id)) {
  194. seen[module.id] = true;
  195. result.push(module);
  196. }
  197. });
  198. return result;
  199. });
  200. });
  201. }
  202. };
  203. exports.ModuleReader = ModuleReader;
  204. function warnMissingModule(id) {
  205. // A missing module may be a false positive and therefore does not warrant
  206. // a fatal error, but a warning is certainly in order.
  207. util.log.err(
  208. "unable to resolve module " + JSON.stringify(id) + "; false positive?",
  209. "yellow");
  210. // Missing modules are installed as if they existed, but it's a run-time
  211. // error if one is ever actually required.
  212. var message = "nonexistent module required: " + id;
  213. return "throw new Error(" + JSON.stringify(message) + ");";
  214. }
  215. function Module(reader, id, hash, output) {
  216. assert.ok(this instanceof Module);
  217. assert.ok(reader instanceof ModuleReader);
  218. assert.strictEqual(typeof output, "object");
  219. var source = output[".js"];
  220. assert.strictEqual(typeof source, "string");
  221. Object.defineProperties(this, {
  222. reader: { value: reader },
  223. id: { value: id },
  224. hash: { value: hash }, // TODO Remove?
  225. deps: { value: getRequiredIDs(id, source) },
  226. source: { value: source },
  227. output: { value: output }
  228. });
  229. }
  230. Module.prototype = {
  231. getRequiredP: function() {
  232. return this.reader.readMultiP(this.deps);
  233. },
  234. writeVersionP: function(outputDir) {
  235. var id = this.id;
  236. var hash = this.hash;
  237. var output = this.output;
  238. var cacheDir = this.reader.context.cacheDir;
  239. var charset = this.reader.context.options.outputCharset;
  240. return Q.all(Object.keys(output).map(function(key) {
  241. var outputFile = path.join(outputDir, id + key);
  242. function writeCopy() {
  243. if (charset) {
  244. fs.writeFileSync(outputFile, iconv.encode(output[key], charset));
  245. } else {
  246. fs.writeFileSync(outputFile, output[key], "utf8");
  247. }
  248. return outputFile;
  249. }
  250. if (cacheDir) {
  251. var cacheFile = path.join(cacheDir, hash + key);
  252. return util.linkP(cacheFile, outputFile)
  253. // If the hard linking fails, the cache directory
  254. // might be on a different device, so fall back to
  255. // writing a copy of the file (slightly slower).
  256. .catch(writeCopy);
  257. }
  258. return util.mkdirP(path.dirname(outputFile)).then(writeCopy);
  259. }));
  260. },
  261. toString: function() {
  262. return "Module(" + JSON.stringify(this.id) + ")";
  263. },
  264. resolveId: function(id) {
  265. return util.absolutize(this.id, id);
  266. }
  267. };