123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- var assert = require("assert");
- var path = require("path");
- var fs = require("fs");
- var Q = require("q");
- var iconv = require("iconv-lite");
- var ReadFileCache = require("./cache").ReadFileCache;
- var Watcher = require("./watcher").Watcher;
- var contextModule = require("./context");
- var BuildContext = contextModule.BuildContext;
- var PreferredFileExtension = contextModule.PreferredFileExtension;
- var ModuleReader = require("./reader").ModuleReader;
- var output = require("./output");
- var DirOutput = output.DirOutput;
- var StdOutput = output.StdOutput;
- var util = require("./util");
- var log = util.log;
- var Ap = Array.prototype;
- var each = Ap.forEach;
- // Better stack traces for promises.
- Q.longStackSupport = true;
- function Commoner() {
- var self = this;
- assert.ok(self instanceof Commoner);
- Object.defineProperties(self, {
- customVersion: { value: null, writable: true },
- customOptions: { value: [] },
- resolvers: { value: [] },
- processors: { value: [] }
- });
- }
- var Cp = Commoner.prototype;
- Cp.version = function(version) {
- this.customVersion = version;
- return this; // For chaining.
- };
- // Add custom command line options
- Cp.option = function() {
- this.customOptions.push(Ap.slice.call(arguments));
- return this; // For chaining.
- };
- // A resolver is a function that takes a module identifier and returns
- // the unmodified source of the corresponding module, either as a string
- // or as a promise for a string.
- Cp.resolve = function() {
- each.call(arguments, function(resolver) {
- assert.strictEqual(typeof resolver, "function");
- this.resolvers.push(resolver);
- }, this);
- return this; // For chaining.
- };
- // A processor is a function that takes a module identifier and a string
- // representing the source of the module and returns a modified version of
- // the source, either as a string or as a promise for a string.
- Cp.process = function(processor) {
- each.call(arguments, function(processor) {
- assert.strictEqual(typeof processor, "function");
- this.processors.push(processor);
- }, this);
- return this; // For chaining.
- };
- Cp.buildP = function(options, roots) {
- var self = this;
- var sourceDir = options.sourceDir;
- var outputDir = options.outputDir;
- var readFileCache = new ReadFileCache(sourceDir, options.sourceCharset);
- var waiting = 0;
- var output = outputDir
- ? new DirOutput(outputDir)
- : new StdOutput;
- if (self.watch) {
- new Watcher(readFileCache).on("changed", function(file) {
- log.err(file + " changed; rebuilding...", "yellow");
- rebuild();
- });
- }
- function outputModules(modules) {
- // Note that output.outputModules comes pre-bound.
- modules.forEach(output.outputModule);
- return modules;
- }
- function finish(result) {
- rebuild.ing = false;
- if (waiting > 0) {
- waiting = 0;
- process.nextTick(rebuild);
- }
- return result;
- }
- function rebuild() {
- if (rebuild.ing) {
- waiting += 1;
- return;
- }
- rebuild.ing = true;
- var context = new BuildContext(options, readFileCache);
- if (self.preferredFileExtension)
- context.setPreferredFileExtension(
- self.preferredFileExtension);
- context.setCacheDirectory(self.cacheDir);
- context.setIgnoreDependencies(self.ignoreDependencies);
- context.setRelativize(self.relativize);
- context.setUseProvidesModule(self.useProvidesModule);
- return new ModuleReader(
- context,
- self.resolvers,
- self.processors
- ).readMultiP(context.expandIdsOrGlobsP(roots))
- .then(context.ignoreDependencies ? pass : collectDepsP)
- .then(outputModules)
- .then(outputDir ? printModuleIds : pass)
- .then(finish, function(err) {
- log.err(err.stack);
- if (!self.watch) {
- // If we're not building with --watch, throw the error
- // so that cliBuildP can call process.exit(-1).
- throw err;
- }
- finish();
- });
- }
- return (
- // If outputDir is falsy, we can't (and don't need to) mkdirP it.
- outputDir ? util.mkdirP : Q
- )(outputDir).then(rebuild);
- };
- function pass(modules) {
- return modules;
- }
- function collectDepsP(rootModules) {
- var modules = [];
- var seenIds = {};
- function traverse(module) {
- if (seenIds.hasOwnProperty(module.id))
- return Q(modules);
- seenIds[module.id] = true;
- return module.getRequiredP().then(function(reqs) {
- return Q.all(reqs.map(traverse));
- }).then(function() {
- modules.push(module);
- return modules;
- });
- }
- return Q.all(rootModules.map(traverse)).then(
- function() { return modules });
- }
- function printModuleIds(modules) {
- log.out(JSON.stringify(modules.map(function(module) {
- return module.id;
- })));
- return modules;
- }
- Cp.forceResolve = function(forceId, source) {
- this.resolvers.unshift(function(id) {
- if (id === forceId)
- return source;
- });
- };
- Cp.cliBuildP = function() {
- var version = this.customVersion || require("../package.json").version;
- return Q.spread([this, version], cliBuildP);
- };
- function cliBuildP(commoner, version) {
- var options = require("commander");
- var workingDir = process.cwd();
- var sourceDir = workingDir;
- var outputDir = null;
- var roots;
- options.version(version)
- .usage("[options] <source directory> <output directory> [<module ID> [<module ID> ...]]")
- .option("-c, --config [file]", "JSON configuration file (no file or - means STDIN)")
- .option("-w, --watch", "Continually rebuild")
- .option("-x, --extension <js | coffee | ...>",
- "File extension to assume when resolving module identifiers")
- .option("--relativize", "Rewrite all module identifiers to be relative")
- .option("--follow-requires", "Scan modules for required dependencies")
- .option("--use-provides-module", "Respect @providesModules pragma in files")
- .option("--cache-dir <directory>", "Alternate directory to use for disk cache")
- .option("--no-cache-dir", "Disable the disk cache")
- .option("--source-charset <utf8 | win1252 | ...>",
- "Charset of source (default: utf8)")
- .option("--output-charset <utf8 | win1252 | ...>",
- "Charset of output (default: utf8)");
- commoner.customOptions.forEach(function(customOption) {
- options.option.apply(options, customOption);
- });
- options.parse(process.argv.slice(0));
- var pfe = new PreferredFileExtension(options.extension || "js");
- // TODO Decide whether passing options to buildP via instance
- // variables is preferable to passing them as arguments.
- commoner.preferredFileExtension = pfe;
- commoner.watch = options.watch;
- commoner.ignoreDependencies = !options.followRequires;
- commoner.relativize = options.relativize;
- commoner.useProvidesModule = options.useProvidesModule;
- commoner.sourceCharset = normalizeCharset(options.sourceCharset);
- commoner.outputCharset = normalizeCharset(options.outputCharset);
- function fileToId(file) {
- file = absolutePath(workingDir, file);
- assert.ok(fs.statSync(file).isFile(), file);
- return pfe.trim(path.relative(sourceDir, file));
- }
- var args = options.args.slice(0);
- var argc = args.length;
- if (argc === 0) {
- if (options.config === true) {
- log.err("Cannot read --config from STDIN when reading " +
- "source from STDIN");
- process.exit(-1);
- }
- sourceDir = workingDir;
- outputDir = null;
- roots = ["<stdin>"];
- commoner.forceResolve("<stdin>", util.readFromStdinP());
- // Ignore dependencies because we wouldn't know how to find them.
- commoner.ignoreDependencies = true;
- } else {
- var first = absolutePath(workingDir, args[0]);
- var stats = fs.statSync(first);
- if (argc === 1) {
- var firstId = fileToId(first);
- sourceDir = workingDir;
- outputDir = null;
- roots = [firstId];
- commoner.forceResolve(
- firstId,
- util.readFileP(first, commoner.sourceCharset)
- );
- // Ignore dependencies because we wouldn't know how to find them.
- commoner.ignoreDependencies = true;
- } else if (stats.isDirectory(first)) {
- sourceDir = first;
- outputDir = absolutePath(workingDir, args[1]);
- roots = args.slice(2);
- if (roots.length === 0)
- roots.push(commoner.preferredFileExtension.glob());
- } else {
- options.help();
- process.exit(-1);
- }
- }
- commoner.cacheDir = null;
- if (options.cacheDir === false) {
- // Received the --no-cache-dir option, so disable the disk cache.
- } else if (typeof options.cacheDir === "string") {
- commoner.cacheDir = absolutePath(workingDir, options.cacheDir);
- } else if (outputDir) {
- // The default cache directory lives inside the output directory.
- commoner.cacheDir = path.join(outputDir, ".module-cache");
- }
- var promise = getConfigP(
- workingDir,
- options.config
- ).then(function(config) {
- var cleanOptions = {};
- options.options.forEach(function(option) {
- var name = util.camelize(option.name());
- if (options.hasOwnProperty(name)) {
- cleanOptions[name] = options[name];
- }
- });
- cleanOptions.version = version;
- cleanOptions.config = config;
- cleanOptions.sourceDir = sourceDir;
- cleanOptions.outputDir = outputDir;
- cleanOptions.sourceCharset = commoner.sourceCharset;
- cleanOptions.outputCharset = commoner.outputCharset;
- return commoner.buildP(cleanOptions, roots);
- });
- if (!commoner.watch) {
- // If we're building from the command line without --watch, any
- // build errors should immediately terminate the process with a
- // non-zero error code.
- promise = promise.catch(function(err) {
- log.err(err.stack);
- process.exit(-1);
- });
- }
- return promise;
- }
- function normalizeCharset(charset) {
- charset = charset
- && charset.replace(/[- ]/g, "").toLowerCase()
- || "utf8";
- assert.ok(
- iconv.encodingExists(charset),
- "Unrecognized charset: " + charset
- );
- return charset;
- }
- function absolutePath(workingDir, pathToJoin) {
- if (pathToJoin) {
- workingDir = path.normalize(workingDir);
- pathToJoin = path.normalize(pathToJoin);
- // TODO: use path.isAbsolute when Node < 0.10 is unsupported
- if (path.resolve(pathToJoin) !== pathToJoin) {
- pathToJoin = path.join(workingDir, pathToJoin);
- }
- }
- return pathToJoin;
- }
- function getConfigP(workingDir, configFile) {
- if (typeof configFile === "undefined")
- return Q({}); // Empty config.
- if (configFile === true || // --config is present but has no argument
- configFile === "<stdin>" ||
- configFile === "-" ||
- configFile === path.sep + path.join("dev", "stdin")) {
- return util.readJsonFromStdinP(
- 1000, // Time limit in milliseconds before warning displayed.
- "Expecting configuration from STDIN (pass --config <file> " +
- "if stuck here)...",
- "yellow"
- );
- }
- return util.readJsonFileP(absolutePath(workingDir, configFile));
- }
- exports.Commoner = Commoner;
|