var assert = require("assert"); var path = require("path"); var fs = require("fs"); var Q = require("q"); var iconv = require("iconv-lite"); var createHash = require("crypto").createHash; var detective = require("detective"); var util = require("./util"); var BuildContext = require("./context").BuildContext; var slice = Array.prototype.slice; function getRequiredIDs(id, source) { var ids = {}; detective(source).forEach(function (dep) { ids[path.normalize(path.join(id, "..", dep))] = true; }); return Object.keys(ids); } function ModuleReader(context, resolvers, processors) { var self = this; assert.ok(self instanceof ModuleReader); assert.ok(context instanceof BuildContext); assert.ok(resolvers instanceof Array); assert.ok(processors instanceof Array); var hash = createHash("sha1").update(context.optionsHash + "\0"); function hashCallbacks(salt) { hash.update(salt + "\0"); var cbs = util.flatten(slice.call(arguments, 1)); cbs.forEach(function(cb) { assert.strictEqual(typeof cb, "function"); hash.update(cb + "\0"); }); return cbs; } resolvers = hashCallbacks("resolvers", resolvers, warnMissingModule); var procArgs = [processors]; if (context.relativize && !context.ignoreDependencies) procArgs.push(require("./relative").getProcessor(self)); processors = hashCallbacks("processors", procArgs); Object.defineProperties(self, { context: { value: context }, idToHash: { value: {} }, resolvers: { value: resolvers }, processors: { value: processors }, salt: { value: hash.digest("hex") } }); } ModuleReader.prototype = { getSourceP: util.cachedMethod(function(id) { var context = this.context; var copy = this.resolvers.slice(0).reverse(); assert.ok(copy.length > 0, "no source resolvers registered"); function tryNextResolverP() { var resolve = copy.pop(); try { var promise = Q(resolve && resolve.call(context, id)); } catch (e) { promise = Q.reject(e); } return resolve ? promise.then(function(result) { if (typeof result === "string") return result; return tryNextResolverP(); }, tryNextResolverP) : promise; } return tryNextResolverP(); }), getCanonicalIdP: util.cachedMethod(function(id) { var reader = this; if (reader.context.useProvidesModule) { return reader.getSourceP(id).then(function(source) { return reader.context.getProvidedId(source) || id; }); } else { return Q(id); } }), readModuleP: util.cachedMethod(function(id) { var reader = this; return reader.getSourceP(id).then(function(source) { if (reader.context.useProvidesModule) { // If the source contains a @providesModule declaration, treat // that declaration as canonical. Note that the Module object // returned by readModuleP might have an .id property whose // value differs from the original id parameter. id = reader.context.getProvidedId(source) || id; } assert.strictEqual(typeof source, "string"); var hash = createHash("sha1") .update("module\0") .update(id + "\0") .update(reader.salt + "\0") .update(source.length + "\0" + source) .digest("hex"); if (reader.idToHash.hasOwnProperty(id)) { // Ensure that the same module identifier is not // provided by distinct modules. assert.strictEqual( reader.idToHash[id], hash, "more than one module named " + JSON.stringify(id)); } else { reader.idToHash[id] = hash; } return reader.buildModuleP(id, hash, source); }); }), buildModuleP: util.cachedMethod(function(id, hash, source) { var reader = this; return reader.processOutputP( id, hash, source ).then(function(output) { return new Module(reader, id, hash, output); }); }, function(id, hash, source) { return hash; }), processOutputP: function(id, hash, source) { var reader = this; var cacheDir = reader.context.cacheDir; var manifestDir = cacheDir && path.join(cacheDir, "manifest"); var charset = reader.context.options.outputCharset; function buildP() { var promise = Q(source); reader.processors.forEach(function(build) { promise = promise.then(function(input) { return util.waitForValuesP( build.call(reader.context, id, input) ); }); }); return promise.then(function(output) { if (typeof output === "string") { output = { ".js": output }; } else { assert.strictEqual(typeof output, "object"); } return util.waitForValuesP(output); }).then(function(output) { util.log.err( "built Module(" + JSON.stringify(id) + ")", "cyan" ); return output; }).catch(function(err) { // Provide additional context for uncaught build errors. util.log.err("Error while reading module " + id + ":"); throw err; }); } if (manifestDir) { return util.mkdirP(manifestDir).then(function(manifestDir) { var manifestFile = path.join(manifestDir, hash + ".json"); return util.readJsonFileP(manifestFile).then(function(manifest) { Object.keys(manifest).forEach(function(key) { var cacheFile = path.join(cacheDir, manifest[key]); manifest[key] = util.readFileP(cacheFile); }); return util.waitForValuesP(manifest, true); }).catch(function(err) { return buildP().then(function(output) { var manifest = {}; Object.keys(output).forEach(function(key) { var cacheFile = manifest[key] = hash + key; var fullPath = path.join(cacheDir, cacheFile); if (charset) { fs.writeFileSync(fullPath, iconv.encode(output[key], charset)) } else { fs.writeFileSync(fullPath, output[key], "utf8"); } }); fs.writeFileSync( manifestFile, JSON.stringify(manifest), "utf8" ); return output; }); }); }); } return buildP(); }, readMultiP: function(ids) { var reader = this; return Q(ids).all().then(function(ids) { if (ids.length === 0) return ids; // Shortcut. var modulePs = ids.map(reader.readModuleP, reader); return Q(modulePs).all().then(function(modules) { var seen = {}; var result = []; modules.forEach(function(module) { if (!seen.hasOwnProperty(module.id)) { seen[module.id] = true; result.push(module); } }); return result; }); }); } }; exports.ModuleReader = ModuleReader; function warnMissingModule(id) { // A missing module may be a false positive and therefore does not warrant // a fatal error, but a warning is certainly in order. util.log.err( "unable to resolve module " + JSON.stringify(id) + "; false positive?", "yellow"); // Missing modules are installed as if they existed, but it's a run-time // error if one is ever actually required. var message = "nonexistent module required: " + id; return "throw new Error(" + JSON.stringify(message) + ");"; } function Module(reader, id, hash, output) { assert.ok(this instanceof Module); assert.ok(reader instanceof ModuleReader); assert.strictEqual(typeof output, "object"); var source = output[".js"]; assert.strictEqual(typeof source, "string"); Object.defineProperties(this, { reader: { value: reader }, id: { value: id }, hash: { value: hash }, // TODO Remove? deps: { value: getRequiredIDs(id, source) }, source: { value: source }, output: { value: output } }); } Module.prototype = { getRequiredP: function() { return this.reader.readMultiP(this.deps); }, writeVersionP: function(outputDir) { var id = this.id; var hash = this.hash; var output = this.output; var cacheDir = this.reader.context.cacheDir; var charset = this.reader.context.options.outputCharset; return Q.all(Object.keys(output).map(function(key) { var outputFile = path.join(outputDir, id + key); function writeCopy() { if (charset) { fs.writeFileSync(outputFile, iconv.encode(output[key], charset)); } else { fs.writeFileSync(outputFile, output[key], "utf8"); } return outputFile; } if (cacheDir) { var cacheFile = path.join(cacheDir, hash + key); return util.linkP(cacheFile, outputFile) // If the hard linking fails, the cache directory // might be on a different device, so fall back to // writing a copy of the file (slightly slower). .catch(writeCopy); } return util.mkdirP(path.dirname(outputFile)).then(writeCopy); })); }, toString: function() { return "Module(" + JSON.stringify(this.id) + ")"; }, resolveId: function(id) { return util.absolutize(this.id, id); } };