var _ = require('underscore');
var sourcemap = require('source-map');

var files = require('./files.js');
var utils = require('./utils.js');
var watch = require('./watch.js');
var buildmessage = require('./buildmessage.js');
var meteorNpm = require('./meteor-npm.js');
var NpmDiscards = require('./npm-discards.js');
var Builder = require('./builder.js');
var archinfo = require('./archinfo.js');
var release = require('./release.js');
var catalog = require('./catalog.js');
var packageVersionParser = require('./package-version-parser.js');
var compiler = require('./compiler.js');
var packageAPIModule = require('./package-api.js');
var Profile = require('./profile.js').Profile;

// XXX: This is a medium-term hack, to avoid having the user set a package name
// & test-name in package.describe. We will change this in the new control file
// world in some way.
var AUTO_TEST_PREFIX = "local-test:";
var isTestName = function (name) {
  var nameStart = name.slice(0, AUTO_TEST_PREFIX.length);
  return nameStart === AUTO_TEST_PREFIX;
};
var genTestName = function (name) {
  return AUTO_TEST_PREFIX + name;
};

// Returns a sort comparator to order files into load order.
var loadOrderSort = function (sourceProcessorSet, arch) {
  const isTemplate = _.memoize((filename) => {
    const classification = sourceProcessorSet.classifyFilename(filename, arch);
    switch (classification.type) {
    case 'extension':
    case 'filename':
      if (! classification.sourceProcessors) {
        // This is *.js, not a template. #HardcodeJs
        return false;
      }
      if (classification.sourceProcessors.length > 1) {
        throw Error("conflicts in compiler?");
      }
      return classification.sourceProcessors[0].isTemplate;

    case 'legacy-handler':
      return classification.legacyIsTemplate;

    case 'wrong-arch':
    case 'unmatched':
      return false;

    default:
      throw Error(`surprising type ${classification.type} for ${filename}`);
    }
  });

  return function (a, b) {
    // XXX MODERATELY SIZED HACK --
    // push template files ahead of everything else. this is
    // important because the user wants to be able to say
    //   Template.foo.events = { ... }
    // in a JS file and not have to worry about ordering it
    // before the corresponding .html file.
    //
    // maybe all of the templates should go in one file?
    var isTemplate_a = isTemplate(files.pathBasename(a));
    var isTemplate_b = isTemplate(files.pathBasename(b));
    if (isTemplate_a !== isTemplate_b) {
      return (isTemplate_a ? -1 : 1);
    }

    // main.* loaded last
    var ismain_a = (files.pathBasename(a).indexOf('main.') === 0);
    var ismain_b = (files.pathBasename(b).indexOf('main.') === 0);
    if (ismain_a !== ismain_b) {
      return (ismain_a ? 1 : -1);
    }

    // /lib/ loaded first
    var islib_a = (a.indexOf(files.pathSep + 'lib' + files.pathSep) !== -1 ||
                   a.indexOf('lib' + files.pathSep) === 0);
    var islib_b = (b.indexOf(files.pathSep + 'lib' + files.pathSep) !== -1 ||
                   b.indexOf('lib' + files.pathSep) === 0);
    if (islib_a !== islib_b) {
      return (islib_a ? -1 : 1);
    }

    var a_parts = a.split(files.pathSep);
    var b_parts = b.split(files.pathSep);

    // deeper paths loaded first.
    var len_a = a_parts.length;
    var len_b = b_parts.length;
    if (len_a < len_b) return 1;
    if (len_b < len_a) return -1;

    // Otherwise compare path components lexicographically.
    for (var i = 0; i < len_a; ++i) {
      var a_part = a_parts[i];
      var b_part = b_parts[i];
      if (a_part < b_part) return -1;
      if (b_part < a_part) return 1;
    }

    // Never reached unless there are somehow duplicate paths.
    return 0;
  };
};

var splitConstraint = function (c) {
  // XXX print error better (w/ buildmessage?)?
  var parsed = utils.parsePackageConstraint(c);
  return { package: parsed.package,
           constraint: parsed.constraintString || null };
};

// Given the text of a README.md file, excerpts the text between the first and
// second heading.
//
// Specifically - if there is text between the document name, and the first
// subheading, it will take that text. If there is no text there, and only text
// after the first subheading, it will take that text. It won't look any deeper
// than that (in case the user intentionally wants to leave the section blank
// for some reason). Skips lines that start with an exclamation point.
var getExcerptFromReadme = function (text) {
  // Don't waste time parsing if the document is empty.
  if (! text) return "";

  // Split into lines with Commonmark.
  var commonmark = require('commonmark');
  var reader = new commonmark.DocParser();
  var parsed = reader.parse(text);

  // Commonmark will parse the Markdown into an array of nodes. These are the
  // nodes that represent the text between the first and second heading.
  var relevantNodes = [];

  // Go through the document until we get the nodes that we are looking for,
  // then stop.
  _.any(parsed.children, function (child) {
    var isHeader = (child.t === "Header");
    // Don't excerpt anything before the first header.
    if (! isHeader) {
      // If we are currently in the middle of excerpting, continue doing that
      // until we hit hit a header (and this is not a header). Otherwise, if
      // this is text, we should begin to excerpt it.
      relevantNodes.push(child);
    } else if (! _.isEmpty(relevantNodes) && isHeader) {
      // We have been excerpting, and came across a header. That means
      // that we are done.
      return true;
    }
    return false;
  });

  // If we have not found anything, we are done.
  if (_.isEmpty(relevantNodes)) return "";

  // For now, we will do the simple thing of just taking the raw markdown from
  // the start of the excerpt to the end.
  var textLines = text.split("\n");
  var start = relevantNodes[0].start_line - 1;
  var stop = _.last(relevantNodes).end_line;
  // XXX: There is a bug in commonmark that happens when processing the last
  // node in the document. Here is the github issue:
  // https://github.com/jgm/CommonMark/issues/276
  // Remove this workaround when the issue is fixed.
  if (stop === _.last(parsed.children).end_line) {
    stop++;
  }
  var excerpt = textLines.slice(start, stop).join("\n");

  // Strip the preceeding and trailing new lines.
  return excerpt.replace(/^\n+|\n+$/g, "");
};

///////////////////////////////////////////////////////////////////////////////
// SourceArch
///////////////////////////////////////////////////////////////////////////////

// Options:
// - kind [required]
// - arch [required]
// - uses
// - implies
// - getSourcesFunc
// - declaredExports
// - watchSet
//
// Do not include the source files in watchSet. They will be
// added at compile time when the sources are actually read.
var SourceArch = function (pkg, options) {
  var self = this;
  options = options || {};
  self.pkg = pkg;

  // Kind of this sourceArchitecture. At the moment, there are really three
  // options -- package, plugin, and app. We use these in linking.
  self.kind = options.kind;

  // The architecture (fully or partially qualified) that can use this
  // unibuild.
  self.arch = options.arch;

  // Packages used. The ordering is significant only for determining
  // import symbol priority (it doesn't affect load order), and a
  // given package could appear more than once in the list, so code
  // that consumes this value will need to guard appropriately. Each
  // element in the array has keys:
  // - package: the package name
  // - constraint: the constraint on the version of the package to use,
  //   as a string (may be null)
  // - unordered: If true, we don't want the package's imports and we
  //   don't want to force the package to load before us. We just want
  //   to ensure that it loads if we load.
  // - weak: If true, we don't *need* to load the other package, but
  //   if the other package ends up loaded in the target, it must
  //   be forced to load before us. We will not get its imports
  //   or plugins.
  // It is an error for both unordered and weak to be true, because
  // such a dependency would have no effect.
  //
  // In most places, instead of using 'uses' directly, you want to use
  // something like compiler.eachUsedUnibuild so you also take into
  // account implied packages.
  //
  // Note that if `package` starts with 'isobuild:', it actually represents a
  // dependency on a feature of the Isobuild build tool, not a real package. You
  // need to be aware of this when processing a `uses` array, which is another
  // reason to use eachUsedUnibuild instead.
  self.uses = options.uses || [];

  // Packages which are "implied" by using this package. If a unibuild X
  // uses this unibuild Y, and Y implies Z, then X will effectively use Z
  // as well (and get its imports and plugins).  An array of objects
  // of the same type as the elements of self.uses (although for now
  // unordered and weak are not allowed).
  self.implies = options.implies || [];

  // A function that returns the source files for this architecture. Array of
  // objects with keys "relPath" and "fileOptions". Null if loaded from
  // isopack.
  //
  // fileOptions is optional and represents arbitrary options passed
  // to "api.addFiles"; they are made available on to the plugin as
  // compileStep.fileOptions.
  //
  // This is a function rather than a literal array because for an
  // app, we need to know the file extensions registered by the
  // plugins in order to compute the sources list, so we have to wait
  // until build time (after we have loaded any plugins, including
  // local plugins in this package) to compute this.
  self.getSourcesFunc = options.getSourcesFunc || null;

  // Symbols that this architecture should export. List of symbols (as
  // strings).
  self.declaredExports = options.declaredExports || null;

  // Files and directories that we want to monitor for changes in
  // development mode, as a watch.WatchSet. In the latest refactoring
  // of the code, this does not include source files or directories,
  // but only control files such as package.js and .meteor/packages,
  // since the rest are not determined until compile time.
  self.watchSet = options.watchSet || new watch.WatchSet;
};

///////////////////////////////////////////////////////////////////////////////
// PackageSource
///////////////////////////////////////////////////////////////////////////////

var PackageSource = function () {
  var self = this;

  // The name of the package, or null for an app pseudo-package or
  // collection. The package's exports will reside in Package.<name>.
  // When it is null it is linked like an application instead of like
  // a package.
  self.name = null;

  // The path relative to which all source file paths are interpreted
  // in this package. Also used to compute the location of the
  // package's .npm directory (npm shrinkwrap state).
  self.sourceRoot = null;

  // Path that will be prepended to the URLs of all resources emitted
  // by this package (assuming they don't end up getting
  // concatenated). For non-web targets, the only effect this will
  // have is to change the actual on-disk paths of the files in the
  // bundle, for those that care to open up the bundle and look (but
  // it's still nice to get it right).
  self.serveRoot = null;

  // Package metadata. Keys are 'summary', 'git' and 'documentation'. Currently
  // all of these are optional.
  self.metadata = {};
  self.docsExplicitlyProvided = false;

  // Package version as a meteor-version string. Optional; not all packages
  // (for example, the app) have versions.
  // XXX when we have names, maybe we want to say that all packages
  // with names have versions? certainly the reverse is true
  self.version = null;
  self.versionExplicitlyProvided = false;

  // Available architectures of this package. Array of SourceArch.
  self.architectures = [];

  // The information necessary to build the plugins in this
  // package. Map from plugin name to object with keys 'name', 'use',
  // 'sources', and 'npmDependencies'.
  self.pluginInfo = {};

  // Analogous to watchSet in SourceArch but for plugins. At this
  // stage will typically contain just 'package.js'.
  self.pluginWatchSet = new watch.WatchSet;

  // npm packages used by this package (on os.* architectures only).
  // Map from npm package name to the required version of the package
  // as a string.
  self.npmDependencies = {};

  // Files to be stripped from the installed NPM dependency tree. See the
  // Npm.strip comment below for further usage information.
  self.npmDiscards = new NpmDiscards;

  // Absolute path to a directory on disk that serves as a cache for
  // the npm dependencies, so we don't have to fetch them on every
  // build. Required not just if we have npmDependencies, but if we
  // ever could have had them in the past.
  self.npmCacheDirectory = null;

  // cordova plugins used by this package (on os.* architectures only).
  // Map from cordova plugin name to the required version of the package
  // as a string.
  self.cordovaDependencies = {};

  // If this package has a corresponding test package (for example,
  // underscore-test), defined in the same package.js file, store its value
  // here.
  self.testName = null;

  // Test packages are dealt with differently in the linker (and not published
  // to the catalog), so we need to keep track of them.
  self.isTest = false;

  // Some packages belong to a test framework and should never be bundled into
  // production. A package with this flag should not be picked up by the bundler
  // for production builds.
  self.debugOnly = false;

  // If this is set, we will take the currently running git checkout and bundle
  // the meteor tool from it inside this package as a tool. We will include
  // built copies of all known isopackets.
  self.includeTool = false;

  // Is this a core package? Core packages don't record version files, because
  // core packages are only ever run from checkout. For the preview release,
  // core packages do not need to specify their versions at publication (since
  // there isn't likely to be any exciting version skew yet), but we will
  // specify the correct restrictions at 0.90.
  // XXX: 0.90 package versions.
  self.isCore = false;
};

_.extend(PackageSource.prototype, {
  // Make a dummy (empty) packageSource that contains nothing of interest.
  // XXX: Do we need this
  initEmpty: function (name) {
    var self = this;
    self.name = name;
  },

  // Programmatically initialize a PackageSource from scratch.
  //
  // Unlike user-facing methods of creating a package
  // (initFromPackageDir, initFromAppDir) this does not implicitly add
  // a dependency on the 'meteor' package. If you want such a
  // dependency then you must add it yourself.
  //
  // If called inside a buildmessage job, it will keep going if things
  // go wrong. Be sure to call jobHasMessages to see if it actually
  // succeeded.
  //
  // The architecture is hardcoded to be "os".
  //
  // Note that this does not set a version on the package!
  //
  // Options:
  // - sourceRoot (required if sources present)
  // - serveRoot (required if sources present)
  // - use
  // - sources (array of paths or relPath/fileOptions objects)
  // - npmDependencies
  // - cordovaDependencies
  // - npmDir
  initFromOptions: function (name, options) {
    var self = this;
    self.name = name;

    if (options.sources && ! _.isEmpty(options.sources) &&
        (! options.sourceRoot || ! options.serveRoot))
      throw new Error("When source files are given, sourceRoot and " +
                      "serveRoot must be specified");

    // sourceRoot is a relative file system path, one slash identifies a root
    // relative to some starting location
    self.sourceRoot = options.sourceRoot || files.pathSep;
    // serveRoot is actually a part of a url path, root here is a forward slash
    self.serveRoot = options.serveRoot || '/';

    var nodeModulesPath = null;
    utils.ensureOnlyExactVersions(options.npmDependencies);
    self.npmDependencies = options.npmDependencies;
    self.npmCacheDirectory = options.npmDir;

    utils.ensureOnlyExactVersions(options.cordovaDependencies);
    self.cordovaDependencies = options.cordovaDependencies;

    var sources = _.map(options.sources, function (source) {
      if (typeof source === "string")
        return {relPath: source};
      return source;
    });

    var sourceArch = new SourceArch(self, {
      kind: options.kind,
      arch: "os",
      uses: _.map(options.use, splitConstraint),
      getSourcesFunc: function () { return sources; },
      nodeModulesPath: nodeModulesPath
    });

    self.architectures.push(sourceArch);

    if (! self._checkCrossUnibuildVersionConstraints())
      throw new Error("only one unibuild, so how can consistency check fail?");
  },

  // Initialize a PackageSource from a package.js-style package directory. Uses
  // the name field provided and the name/test fields in the package.js file to
  // figre out if this is a test package (load from onTest) or a use package
  // (load from onUse).
  //
  // name: name of the package.
  // dir: location of directory on disk.
  // options:
  // - name: override the name of this package with a different name.
  // - buildingIsopackets: true if this is being scanned in the process
  //   of building isopackets
  initFromPackageDir: function (dir, options) {
    var self = this;
    buildmessage.assertInCapture();
    var isPortable = true;
    options = options || {};
    var initFromPackageDirOptions = options;

    // If we know what package we are initializing, we pass in a
    // name. Otherwise, we are intializing the base package specified by 'name:'
    // field in Package.Describe. In that case, it is clearly not a test
    // package. (Though we could be initializing a specific package without it
    // being a test, for a variety of reasons).
    if (options.name) {
      self.isTest = isTestName(options.name);
      self.name = options.name;
    }

    // Give the package a default version. We do not set
    // versionExplicitlyProvided unless the package configuration file actually
    // sets a version.
    self.version = "0.0.0";

    // To make the transition to using README.md files in Isobuild easier, we
    // initialize the documentation directory to README.md by default.
    self.metadata.documentation = "README.md";

    self.sourceRoot = dir;

    // If we are running from checkout we may be looking at a core package. If
    // we are, let's remember this for things like not recording version files.
    if (files.inCheckout()) {
      var packDir = files.pathJoin(files.getCurrentToolsDir(), 'packages');
      if (files.pathDirname(self.sourceRoot) === packDir) {
        self.isCore = true;
      }
    }
    if (! files.exists(self.sourceRoot))
      throw new Error("putative package directory " + dir + " doesn't exist?");

    var fileAndDepLoader = null;
    var npmDependencies = null;
    var cordovaDependencies = null;

    var packageJsPath = files.pathJoin(self.sourceRoot, 'package.js');
    var code = files.readFile(packageJsPath);
    var packageJsHash = watch.sha1(code);

    var hasTests = false;

    // Any package that depends on us needs to be rebuilt if our package.js file
    // changes, because a change to package.js might add or remove a plugin,
    // which could change a file from being handled by plugin vs treated as
    // an asset.
    self.pluginWatchSet.addFile(packageJsPath, packageJsHash);

    // == 'Package' object visible in package.js ==

    /**
     * @global
     * @name  Package
     * @summary The Package object in package.js
     * @namespace
     * @locus package.js
     */
    var Package = {
      // Set package metadata. Options:
      // - summary: for 'meteor list' & package server
      // - version: package version string
      // There used to be a third option documented here,
      // 'environments', but it was never implemented and no package
      // ever used it.

      /**
       * @summary Provide basic package information.
       * @locus package.js
       * @memberOf Package
       * @param {Object} options
       * @param {String} options.summary A concise 1-2 sentence description of
       * the package, required for publication.
       * @param {String} options.version The (extended)
       * [semver](http://www.semver.org) version for your package. Additionally,
       * Meteor allows a wrap number: a positive integer that follows the
       * version number. If you are porting another package that uses semver
       * versioning, you may want to use the original version, postfixed with
       * `_wrapnumber`. For example, `1.2.3_1` or `2.4.5-rc1_4`. Wrap numbers
       * sort after the original numbers: `1.2.3` < `1.2.3_1` < `1.2.3_2` <
       * `1.2.4-rc.0`. If no version is specified, this field defaults to
       * `0.0.0`. If you want to publish your package to the package server, you
       * must specify a version.
       * @param {String} options.name Optional name override. By default, the
       * package name comes from the name of its directory.
       * @param {String} options.git Optional Git URL to the source repository.
       * @param {String} options.documentation Optional Filepath to
       * documentation. Set to 'README.md' by default. Set this to null to submit
       * no documentation.
       */
      describe: function (options) {
        _.each(options, function (value, key) {
          if (key === "summary" ||
              key === "git") {
            self.metadata[key] = value;
          } else if (key === "documentation") {
            self.metadata[key] = value;
            self.docsExplicitlyProvided = true;
          } else if (key === "version") {
            if (typeof(value) !== "string") {
              buildmessage.error("The package version (specified with "
                                 + "Package.describe) must be a string.");
              // Recover by pretending that version was not set.
            } else {
              var goodVersion = true;
              try {
                var parsedVersion = packageVersionParser.getValidServerVersion(
                  value);
              } catch (e) {
                if (!e.versionParserError)
                  throw e;
                buildmessage.error(
                  "The package version " + value + " (specified with Package.describe) "
                    + "is not a valid Meteor package version.\n"
                    + "Valid package versions are semver (see http://semver.org/), "
                    + "optionally followed by '_' and an integer greater or equal to 1.");
                goodVersion = false;
              }
              // Recover by pretending that the version was not set.
            }
            if (goodVersion && parsedVersion !== value) {
              buildmessage.error(
                "The package version (specified with Package.describe) may not "
                  + "contain a plus-separated build ID.");
              // Recover by pretending that the version was not set.
              goodVersion = false;
            }
            if (goodVersion) {
              self.version = value;
              self.versionExplicitlyProvided = true;
            }
          } else if (key === "name" && !self.isTest) {
            if (!self.name) {
              self.name = value;
            } else if (self.name !== value) {
              // Woah, so we requested a non-test package by name, and it is not
              // the name that we find inside. That's super weird.
              buildmessage.error(
                "trying to initialize a nonexistent base package " + value);
            }
          } else if (key === "debugOnly") {
            self.debugOnly = !!value;
          } else {
          // Do nothing. We might want to add some keys later, and we should err
          // on the side of backwards compatibility.
          }
        });
      },

      /**
       * @summary Define package dependencies and expose package methods.
       * @locus package.js
       * @memberOf Package
       * @param {Function} func A function that takes in the package control `api` object, which keeps track of dependencies and exports.
       */
      onUse: function (f) {
        if (!self.isTest) {
          if (fileAndDepLoader) {
            buildmessage.error("duplicate onUse handler; a package may have " +
                               "only one", { useMyCaller: true });
            // Recover by ignoring the duplicate
            return;
          }
          fileAndDepLoader = f;
        }
      },

      /**
       * @deprecated in 0.9.0
       */
      on_use: function (f) {
        this.onUse(f);
      },

      /**
       * @summary Define dependencies and expose package methods for unit tests.
       * @locus package.js
       * @memberOf Package
       * @param {Function} func A function that takes in the package control 'api' object, which keeps track of dependencies and exports.
       */
      onTest: function (f) {
        // If we are not initializing the test package, then we are initializing
        // the normal package and have now noticed that it has tests. So, let's
        // register the test. This is a medium-length hack until we have new
        // control files.
        if (!self.isTest) {
          hasTests = true;
          return;
        }

        // We are initializing the test, so proceed as normal.
        if (self.isTest) {
          if (fileAndDepLoader) {
            buildmessage.error("duplicate onTest handler; a package may have " +
                               "only one", { useMyCaller: true });
            // Recover by ignoring the duplicate
            return;
          }
          fileAndDepLoader = f;
        }
      },

      /**
       * @deprecated in 0.9.0
       */
      on_test: function (f) {
        this.onTest(f);
      },

      // Define a plugin. A plugin extends the build process for
      // targets that use this package. For example, a Coffeescript
      // compiler would be a plugin. A plugin is its own little
      // program, with its own set of source files, used packages, and
      // npm dependencies.
      //
      // This is an experimental API and for now you should assume
      // that it will change frequently and radically (thus the
      // '_transitional_'). For maximum R&D velocity and for the good
      // of the platform, we will push changes that break your
      // packages that use this API. You've been warned.
      //
      // Options:
      // - name: a name for this plugin. required (cosmetic -- string)
      // - use: package to use for the plugin (names, as strings)
      // - sources: sources for the plugin (array of string)
      // - npmDependencies: map from npm package name to required
      //   version (string)

      /**
       * @summary Define a build plugin. A build plugin extends the build
       * process for apps and packages that use this package. For example,
       * the `coffeescript` package uses a build plugin to compile CoffeeScript
       * source files into JavaScript.
       * @param  {Object} [options]
       * @param {String} options.name A cosmetic name, must be unique in the
       * package.
       * @param {String|String[]} options.use Meteor packages that this
       * plugin uses, independent of the packages specified in
       * [api.onUse](#pack_onUse).
       * @param {String[]} options.sources The source files that make up the
       * build plugin, independent from [api.addFiles](#pack_addFiles).
       * @param {Object} options.npmDependencies An object where the keys
       * are NPM package names, and the keys are the version numbers of
       * required NPM packages, just like in [Npm.depends](#Npm-depends).
       * @memberOf Package
       * @locus package.js
       */
      registerBuildPlugin: function (options) {
        // Tests don't have plugins; plugins initialized in the control file
        // belong to the package and not to the test. (This will be less
        // confusing in the new control file format).
        if (self.isTest) {
          return;
        }

        if (! ('name' in options)) {
          buildmessage.error("build plugins require a name",
                             { useMyCaller: true });
          // recover by ignoring plugin
          return;
        }

        if (options.name in self.pluginInfo) {
          buildmessage.error("this package already has a plugin named '" +
                             options.name + "'",
                             { useMyCaller: true });
          // recover by ignoring plugin
          return;
        }

        if (options.name.match(/\.\./) || options.name.match(/[\\\/]/)) {
          buildmessage.error("bad plugin name", { useMyCaller: true });
          // recover by ignoring plugin
          return;
        }

        // XXX probably want further type checking
        self.pluginInfo[options.name] = options;
      },

      /**
       * @deprecated in 0.9.4
       */
      _transitional_registerBuildPlugin: function (options) {
        this.registerBuildPlugin(options);
      },

      includeTool: function () {
        if (!files.inCheckout()) {
          buildmessage.error("Package.includeTool() can only be used with a " +
                             "checkout of meteor");
        } else if (self.includeTool) {
          buildmessage.error("Duplicate includeTool call");
        } else {
          self.includeTool = true;
        }
      }
    };

    // == 'Npm' object visible in package.js ==

    /**
     * @namespace Npm
     * @global
     * @summary The Npm object in package.js and package source files.
     */
    var Npm = {
      /**
       * @summary Specify which [NPM](https://www.npmjs.org/) packages
       * your Meteor package depends on.
       * @param  {Object} dependencies An object where the keys are package
       * names and the values are version numbers in string form or URLs to a
       * git commit by SHA.  You can only depend on exact versions of NPM
       * packages. Example:
       *
       * ```js
       * Npm.depends({
       *   moment: "2.8.3",
       *   async: "https://github.com/caolan/async/archive/71fa2638973dafd8761fa5457c472a312cc820fe.tar.gz"
       * });
       * ```
       * @locus package.js
       * @memberOf  Npm
       */
      depends: function (_npmDependencies) {
        // XXX make npmDependencies be separate between use and test, so that
        // production doesn't have to ship all of the npm modules used by test
        // code
        if (npmDependencies) {
          buildmessage.error("Npm.depends may only be called once per package",
                             { useMyCaller: true });
          // recover by ignoring the Npm.depends line
          return;
        }
        if (typeof _npmDependencies !== 'object') {
          buildmessage.error("the argument to Npm.depends should be an " +
                             "object, like this: {gcd: '0.0.0'}",
                             { useMyCaller: true });
          // recover by ignoring the Npm.depends line
          return;
        }

        // don't allow npm fuzzy versions so that there is complete
        // consistency when deploying a meteor app
        //
        // XXX use something like seal or lockdown to have *complete*
        // confidence we're running the same code?
        try {
          utils.ensureOnlyExactVersions(_npmDependencies);
        } catch (e) {
          buildmessage.error(e.message, { useMyCaller: true, downcase: true });
          // recover by ignoring the Npm.depends line
          return;
        }

        npmDependencies = _npmDependencies;
      },

      // The `Npm.strip` method makes up for packages that have missing
      // or incomplete .npmignore files by telling the bundler to strip out
      // certain unnecessary files and/or directories during `meteor build`.
      //
      // The `discards` parameter should be an object whose keys are
      // top-level package names and whose values are arrays of strings
      // (or regular expressions) that match paths in that package's
      // directory that should be stripped before installation. For
      // example:
      //
      //   Npm.strip({
      //     connect: [/*\.wmv$/],
      //     useragent: ["tests/"]
      //   });
      //
      // This means (1) "remove any files with the `.wmv` extension from
      // the 'connect' package directory" and (2) "remove the 'tests'
      // directory from the 'useragent' package directory."
      strip: function(discards) {
        self.npmDiscards.merge(discards);
      },

      require: function (name) {
        try {
          return require(name); // from the dev bundle
        } catch (e) {
          buildmessage.error(
            "can't find npm module '" + name +
              "'. In package.js, Npm.require can only find built-in modules.",
            { useMyCaller: true });
          // recover by, uh, returning undefined, which is likely to
          // have some knock-on effects
          return undefined;
        }
      }
    };

    // == 'Cordova' object visible in package.js ==

    /**
     * @namespace Cordova
     * @global
     * @summary The Cordova object in package.js.
     */
    var Cordova = {
      /**
       * @summary Specify which [Cordova / PhoneGap](http://cordova.apache.org/)
       * plugins your Meteor package depends on.
       *
       * Plugins are installed from
       * [plugins.cordova.io](http://plugins.cordova.io/), so the plugins and
       * versions specified must exist there. Alternatively, the version
       * can be replaced with a GitHub tarball URL as described in the
       * [Cordova / PhoneGap](https://github.com/meteor/meteor/wiki/Meteor-Cordova-Phonegap-integration#meteor-packages-with-cordovaphonegap-dependencies)
       * page of the Meteor wiki on GitHub.
       * @param  {Object} dependencies An object where the keys are plugin
       * names and the values are version numbers or GitHub tarball URLs
       * in string form.
       * Example:
       *
       * ```js
       * Cordova.depends({
       *   "org.apache.cordova.camera": "0.3.0"
       * });
       * ```
       *
       * Alternatively, with a GitHub URL:
       *
       * ```js
       * Cordova.depends({
       *   "org.apache.cordova.camera":
       *     "https://github.com/apache/cordova-plugin-camera/tarball/d84b875c"
       * });
       * ```
       *
       * @locus package.js
       * @memberOf  Cordova
       */
      depends: function (_cordovaDependencies) {
        // XXX make cordovaDependencies be separate between use and test, so that
        // production doesn't have to ship all of the npm modules used by test
        // code
        if (cordovaDependencies) {
          buildmessage.error("Cordova.depends may only be called once per package",
                             { useMyCaller: true });
          // recover by ignoring the Cordova.depends line
          return;
        }
        if (typeof _cordovaDependencies !== 'object') {
          buildmessage.error("the argument to Cordova.depends should be an " +
                             "object, like this: {gcd: '0.0.0'}",
                             { useMyCaller: true });
          // recover by ignoring the Cordova.depends line
          return;
        }

        // don't allow cordova fuzzy versions so that there is complete
        // consistency when deploying a meteor app
        //
        // XXX use something like seal or lockdown to have *complete*
        // confidence we're running the same code?
        try {
          utils.ensureOnlyExactVersions(_cordovaDependencies);
        } catch (e) {
          buildmessage.error(e.message, { useMyCaller: true, downcase: true });
          // recover by ignoring the Npm.depends line
          return;
        }

        cordovaDependencies = _cordovaDependencies;
      },
    };

    try {
      files.runJavaScript(code.toString('utf8'), {
        filename: 'package.js',
        symbols: { Package: Package, Npm: Npm, Cordova: Cordova }
      });
    } catch (e) {
      buildmessage.exception(e);

      // Could be a syntax error or an exception. Recover by
      // continuing as if package.js is empty. (Pressing on with
      // whatever handlers were registered before the exception turns
      // out to feel pretty disconcerting -- definitely violates the
      // principle of least surprise.) Leave the metadata if we have
      // it, though.
      fileAndDepLoader = null;
      self.pluginInfo = {};
      npmDependencies = null;
      cordovaDependencies = null;
    }

    // In the past, we did not require a Package.Describe.name field. So, it is
    // possible that we are initializing a package that doesn't use it and
    // expects us to be implicit about it.
    if (!self.name) {
      // For backwards-compatibility, we will take the package name from the
      // directory of the package. That was what we used to do: in fact, we used
      // to only do that.
      self.name = files.pathBasename(dir);
    }

    // Check to see if our name is valid.

    try {
      utils.validatePackageName(self.name);
    } catch (e) {
      if (!e.versionParserError)
        throw e;
      buildmessage.error(e.message);
      // recover by ignoring
    }

    // We want the "debug mode" to be a property of the *bundle* operation
    // (turning a set of packages, including the app, into a star), not the
    // *compile* operation (turning a package source into an isopack). This is
    // so we don't have to publish two versions of each package. But we have no
    // way to mark a file in an isopack as being the result of running a plugin
    // from a debugOnly dependency, and so there is no way to tell which files
    // to exclude in production mode from a published package. Eventually, we'll
    // add such a flag to the isopack format, but until then we'll sidestep the
    // issue by disallowing build plugins in debugOnly packages.
    if (self.debugOnly && !_.isEmpty(self.pluginInfo)) {
      buildmessage.error(
        "can't register build plugins in debugOnly packages");
      // recover by ignoring
    }

    // For this old-style, onUse/onTest/where-based package, figure
    // out its dependencies by calling its on_xxx functions and seeing
    // what it does.
    //
    // We have a simple strategy. Call its on_xxx handler with no
    // 'where', which is what happens when the package is added
    // directly to an app, and see what files it adds to the client
    // and the server. When a package is used, include it in both the client
    // and the server by default. This simple strategy doesn't capture even
    // 10% of the complexity possible with onUse, onTest, and where, but
    // probably is sufficient for virtually all packages that actually
    // exist in the field, if not every single one. #OldStylePackageSupport

    var api = new packageAPIModule.PackageAPI({
      buildingIsopackets: !!initFromPackageDirOptions.buildingIsopackets
    });

    if (fileAndDepLoader) {
      try {
        buildmessage.markBoundary(fileAndDepLoader)(api);
      } catch (e) {
        console.log(e.stack); // XXX should we keep this here -- or do we want broken
                              // packages to fail silently?
        buildmessage.exception(e);
        // Recover by ignoring all of the source files in the
        // packages and any remaining handlers. It violates the
        // principle of least surprise to half-run a handler
        // and then continue.
        api.sources = {};
        _.each(compiler.ALL_ARCHES, function (arch) {
          api.sources[arch] = {};
        });

        fileAndDepLoader = null;
        self.pluginInfo = {};
        npmDependencies = null;
        cordovaDependencies = null;
      }
    }

    // By the way, you can't depend on yourself.
    var doNotDepOnSelf = function (dep) {
      if (dep.package === self.name) {
        buildmessage.error("Circular dependency found: "
                           + self.name +
                           " depends on itself.\n");
      }
    };
    _.each(compiler.ALL_ARCHES, function (label) {
      _.each(api.uses[label], doNotDepOnSelf);
      _.each(api.implies[label], doNotDepOnSelf);
    });

    // If we have specified some release, then we should go through the
    // dependencies and fill in the unspecified constraints with the versions in
    // the releases (if possible).
    if (!_.isEmpty(api.releaseRecords)) {

      // Given a dependency object with keys package (the name of the package)
      // and constraint (the version constraint), if the constraint is null,
      // look in the packages field in the release record and fill in from
      // there.
      var setFromRel = function (dep) {
        if (dep.constraint) {
          return dep;
        }
        var newConstraint = [];
        _.each(api.releaseRecords, function (releaseRecord) {
          var packages = releaseRecord.packages;
          if(_.has(packages, dep.package)) {
            newConstraint.push(packages[dep.package]);
          }
        });
        if (_.isEmpty(newConstraint)) return dep;
        dep.constraint = _.reduce(newConstraint,
          function(x, y) {
            return x + " || " + y;
          });
        return dep;
      };

      // For all api.implies and api.uses, fill in the unspecified dependencies from the
      // release.
      _.each(compiler.ALL_ARCHES, function (label) {
        api.uses[label] = _.map(api.uses[label], setFromRel);
        api.implies[label] = _.map(api.implies[label], setFromRel);
      });
     };

    // Make sure that if a dependency was specified in multiple
    // unibuilds, the constraint is exactly the same.
    if (! self._checkCrossUnibuildVersionConstraints()) {
      // A build error was written. Recover by ignoring the
      // fact that we have differing constraints.
    }

    // Save information about npm dependencies. To keep metadata
    // loading inexpensive, we won't actually fetch them until build
    // time.

    // We used to put the cache directly in .npm, but in linker-land,
    // the package's own NPM dependencies go in .npm/package and build
    // plugin X's goes in .npm/plugin/X. Notably, the former is NOT an
    // ancestor of the latter, so that a build plugin does NOT see the
    // package's node_modules.  XXX maybe there should be separate NPM
    // dirs for use vs test?
    self.npmCacheDirectory =
      files.pathResolve(files.pathJoin(self.sourceRoot, '.npm', 'package'));
    self.npmDependencies = npmDependencies;

    self.cordovaDependencies = cordovaDependencies;

    // If this package was previously built with pre-linker versions,
    // it may have files directly inside `.npm` instead of nested
    // inside `.npm/package`. Clean them up if they are there. (Kind
    // of grody to do this here but it'll be fine for now, especially
    // since this is only for compatibility with very old versions of
    // Meteor.)
    var preLinkerFiles = [
      'npm-shrinkwrap.json', 'README', '.gitignore', 'node_modules'];
    _.each(preLinkerFiles, function (f) {
      files.rm_recursive(files.pathJoin(self.sourceRoot, '.npm', f));
    });

    // Create source architectures, one for the server and one for each web
    // arch.
    _.each(compiler.ALL_ARCHES, function (arch) {
      // Everything depends on the package 'meteor', which sets up
      // the basic environment) (except 'meteor' itself, and js-analyze
      // which needs to be loaded by the linker).
      // XXX add a better API for js-analyze to declare itself here
      if (self.name !== "meteor" && self.name !== "js-analyze" &&
          !process.env.NO_METEOR_PACKAGE) {
        // Don't add the dependency if one already exists. This allows the
        // package to create an unordered dependency and override the one that
        // we'd add here. This is necessary to resolve the circular dependency
        // between meteor and underscore (underscore has an unordered
        // dependency on meteor dating from when the .js extension handler was
        // in the "meteor" package).
        var alreadyDependsOnMeteor =
              !! _.find(api.uses[arch], function (u) {
                return u.package === "meteor";
              });
        if (! alreadyDependsOnMeteor)
          api.uses[arch].unshift({ package: "meteor" });
      }

      // Each unibuild has its own separate WatchSet. This is so that, eg, a test
      // unibuild's dependencies doesn't end up getting merged into the
      // pluginWatchSet of a package that uses it: only the use unibuild's
      // dependencies need to go there!
      var watchSet = new watch.WatchSet();
      watchSet.addFile(packageJsPath, packageJsHash);

      self.architectures.push(new SourceArch(self, {
        kind: "main",
        arch: arch,
        uses: api.uses[arch],
        implies: api.implies[arch],
        getSourcesFunc: function () { return _.values(api.sources[arch]); },
        declaredExports: api.exports[arch],
        watchSet: watchSet
      }));
    });

    // Serve root of the package.
    self.serveRoot = files.pathJoin('/packages/', self.name);

    // Name of the test.
    if (hasTests) {
      self.testName = genTestName(self.name);
    }
  },

  // Initialize a package from an application directory (has .meteor/packages).
  initFromAppDir: Profile("initFromAppDir", function (projectContext, ignoreFiles) {
    var self = this;
    var appDir = projectContext.projectDir;
    self.name = null;
    self.sourceRoot = appDir;
    self.serveRoot = '/';

    // special files those are excluded from app's top-level sources
    var controlFiles = ['mobile-config.js'];

    // Determine used packages. Note that these are the same for all arches,
    // because there's no way to specify otherwise in .meteor/packages.
    var uses = [];
    projectContext.projectConstraintsFile.eachConstraint(function (constraint) {
      uses.push({ package: constraint.package,
                  constraint: constraint.constraintString });
    });

    var projectWatchSet = projectContext.getProjectWatchSet();

    _.each(compiler.ALL_ARCHES, function (arch) {
      // We don't need to build a Cordova SourceArch if there are no Cordova
      // platforms.
      if (arch === 'web.cordova' &&
          _.isEmpty(projectContext.platformList.getCordovaPlatforms())) {
        return;
      }

      // XXX what about /web.browser/* etc, these directories could also
      // be for specific client targets.

      // Create unibuild
      var sourceArch = new SourceArch(self, {
        kind: 'app',
        arch: arch,
        uses: uses
      });
      self.architectures.push(sourceArch);

      // sourceArch's WatchSet should include all the project metadata files
      // read by the ProjectContext.
      sourceArch.watchSet.merge(projectWatchSet);

      // Determine source files
      sourceArch.getSourcesFunc = (sourceProcessorSet, watchSet) => {
        const sourceReadOptions =
                sourceProcessorSet.appReadDirectoryOptions(arch);
        // Ignore files starting with dot (unless they are explicitly in
        // 'names').
        sourceReadOptions.exclude.push(/^\./);
        // Ignore the usual ignorable files.
        sourceReadOptions.exclude.push(...ignoreFiles);

        // Wrapper around watch.readAndWatchDirectory which takes in and returns
        // sourceRoot-relative directories.
        var readAndWatchDirectory = (relDir, {include, exclude, names}) => {
          var absPath = files.pathJoin(self.sourceRoot, relDir);
          var contents = watch.readAndWatchDirectory(
            watchSet, {absPath, include, exclude, names});
          return contents.map(x => files.pathJoin(relDir, x));
        };

        // Read top-level source files.
        var sources = readAndWatchDirectory('', sourceReadOptions);

        // don't include watched but not included control files
        sources = _.difference(sources, controlFiles);

        var otherUnibuildRegExp =
              (arch === "os" ? /^client\/$/ : /^server\/$/);

        // The paths that we've called checkForInfiniteRecursion on.
        var seenPaths = {};
        // Used internally by files.realpath as an optimization.
        var realpathCache = {};
        var checkForInfiniteRecursion = function (relDir) {
          var absPath = files.pathJoin(self.sourceRoot, relDir);
          try {
            var realpath = files.realpath(absPath, realpathCache);
          } catch (e) {
            if (!e || e.code !== 'ELOOP')
              throw e;
            // else leave realpath undefined
          }
          if (realpath === undefined || _.has(seenPaths, realpath)) {
            buildmessage.error("Symlink cycle detected at " + relDir);
            // recover by returning no files
            return true;
          }
          seenPaths[realpath] = true;
          return false;
        };

        // Read top-level subdirectories. Ignore subdirectories that have
        // special handling.
        var sourceDirectories = readAndWatchDirectory('', {
          include: [/\/$/],
          exclude: [/^packages\/$/, /^tests\/$/,
                    // XXX We no longer actually have special handling
                    //     for the programs subdirectory, but let's not
                    //     suddenly start treating it as part of the main
                    //     app program.
                    /^programs\/$/,
                    // node.js based tooling often uses dependencies which
                    // are installed into node_modules in the root of the
                    // project.
                    /^node_modules\/$/,
                    /^public\/$/, /^private\/$/,
                    /^cordova-build-override\/$/,
                    otherUnibuildRegExp].concat(sourceReadOptions.exclude)
        });
        checkForInfiniteRecursion('');

        while (!_.isEmpty(sourceDirectories)) {
          var dir = sourceDirectories.shift();

          // remove trailing slash
          dir = dir.substr(0, dir.length - 1);

          if (checkForInfiniteRecursion(dir))
            return [];  // pretend we found no files

          // Find source files in this directory.
          sources.push(...readAndWatchDirectory(dir, sourceReadOptions));

          // Find sub-sourceDirectories. Note that we DON'T need to ignore the
          // directory names that are only special at the top level.
          sourceDirectories.push(...readAndWatchDirectory(dir, {
            include: [/\/$/],
            exclude: [/^tests\/$/, otherUnibuildRegExp].concat(
              sourceReadOptions.exclude)
          }));
        }

        // We've found all the source files. Sort them!
        sources.sort(loadOrderSort(sourceProcessorSet, arch));

        // Convert into relPath/fileOptions objects.
        sources = _.map(sources, function (relPath) {
          var sourceObj = {relPath: relPath};

          // Special case: on the client, JavaScript files in a
          // `client/compatibility` directory don't get wrapped in a closure.
          if (archinfo.matches(arch, "web") && relPath.match(/\.js$/)) {
            var clientCompatSubstr =
              files.pathSep + 'client' +
              files.pathSep + 'compatibility' + files.pathSep;

            if ((files.pathSep + relPath).indexOf(clientCompatSubstr) !== -1)
              sourceObj.fileOptions = {bare: true};
          }
          return sourceObj;
        });

        // Now look for assets for this unibuild.
        const assetDir = archinfo.matches(arch, "web") ? "public/" : "private/";
        var assetDirs = readAndWatchDirectory('', {names: [assetDir]});

        if (!_.isEmpty(assetDirs)) {
          if (!_.isEqual(assetDirs, [assetDir]))
            throw new Error("Surprising assetDirs: " + JSON.stringify(assetDirs));

          while (!_.isEmpty(assetDirs)) {
            dir = assetDirs.shift();
            // remove trailing slash
            dir = dir.substr(0, dir.length - 1);

            if (checkForInfiniteRecursion(dir))
              return [];  // pretend we found no files

            // Find asset files in this directory.
            var assetsAndSubdirs = readAndWatchDirectory(dir, {
              include: [/.?/],
              // we DO look under dot directories here
              exclude: ignoreFiles
            });

            _.each(assetsAndSubdirs, function (item) {
              if (item[item.length - 1] === '/') {
                // Recurse on this directory.
                assetDirs.push(item);
              } else {
                // This file is an asset.
                sources.push({
                  relPath: item,
                  fileOptions: {
                    isAsset: true
                  }
                });
              }
            });
          }
        }

        return sources;
      };
    });

    if (! self._checkCrossUnibuildVersionConstraints()) {
      // should never happen since we created the unibuilds from
      // .meteor/packages, which doesn't have a way to express
      // different constraints for different unibuilds
      throw new Error("conflicting constraints in a package?");
    }
  }),

  // True if the package defines any plugins.
  containsPlugins: function () {
    var self = this;
    return ! _.isEmpty(self.pluginInfo);
  },

  // Return dependency metadata for all unibuilds, in the format needed
  // by the package catalog.
  //
  // This *DOES* include isobuild:* pseudo-packages!
  //
  // Options:
  // - logError: if true, if something goes wrong, log a buildmessage
  //   and return null rather than throwing an exception.
  // - skipWeak: omit weak dependencies
  // - skipUnordered: omit unordered dependencies
  getDependencyMetadata: function (options) {
    var self = this;
    options = options || {};
    var ret = self._computeDependencyMetadata(options);
    if (! ret) {
      if (options.logError)
        return null;
      else
        throw new Error("inconsistent dependency constraint across unibuilds?");
    }
    return ret;
  },

  // Returns a list of package names which should be loaded before building this
  // package. This is all the packages that we directly depend on in a unibuild
  // or from a plugin.
  //
  // (It's possible that we could do something slightly fancier where we only
  // need to load those dependencies (including implied dependencies) which we
  // know contain plugins first, plus the transitive closure of all the packages
  // we depend on which contain a plugin. This seems good enough, though.)
  //
  // Note that this method filters out isobuild:* pseudo-packages, so it is NOT
  // to be used to create input to Version Solver (see
  // _computeDependencyMetadata for that).
  //
  // Note also that "load" here specifically means "load into the IsopackCache
  // at build time", not "load into a running Meteor app at run
  // time". Specifically, weak constraints do create a run-time load order
  // dependency (if the package is in the app at all) but they do not create a
  // build-time IsopackCache load order dependency (because weak dependencies do
  // not provide plugins).
  getPackagesToLoadFirst: function (packageMap) {
    var self = this;
    var packages = {};
    var processUse = function (use) {
      // We don't have to build weak or unordered deps first (eg they can't
      // contribute to a plugin).
      if (use.weak || use.unordered)
        return;
      // Only include real packages, not isobuild:* pseudo-packages.
      if (compiler.isIsobuildFeaturePackage(use.package)) {
        return;
      }

      var packageInfo = packageMap.getInfo(use.package);
      if (! packageInfo)
        throw Error("Depending on unknown package " + use.package);
      packages[use.package] = true;
    };

    _.each(self.architectures, function (arch) {
      // We need to iterate over both uses and implies, since implied packages
      // also constitute dependencies. We don't have to include the dependencies
      // of implied packages directly here, since their own
      // getPackagesToLoadFirst will include those.
      _.each(arch.uses, processUse);
      _.each(arch.implies, processUse);
    });

    _.each(self.pluginInfo, function (info) {
      // info.use is currently just an array of strings, and there's
      // no way to specify weak/unordered. Much like an app.
      _.each(info.use, function (spec) {
        var parsedSpec = splitConstraint(spec);
        if (! compiler.isIsobuildFeaturePackage(parsedSpec.package)) {
          packages[parsedSpec.package] = true;
        }
      });
    });
    return _.keys(packages);
  },

  // Returns an array of objects, representing this package's public
  // exports. Each object has the following keys:
  //  - name: export name (ex: "Accounts")
  //  - arch: an array of strings representing architectures for which this
  //    export is declared.
  //
  // This ignores testOnly exports.
  getExports: function () {
    var self = this;
    var ret = {};
    // Go over all of the architectures, and aggregate the exports together.
    _.each(self.architectures, function (arch) {
      _.each(arch.declaredExports, function (exp) {
        // Skip testOnly exports -- the flag is intended for use in testing
        // only, so it is not of any interest outside this package.
        if (exp.testOnly) {
          return;
        }
        // Add the export to the export map.
        if (! _.has(ret, exp.name)) {
          ret[exp.name] = [arch.arch];
        } else {
          ret[exp.name].push(arch.arch);
        }
     });
    });
    return _.map(ret, function (arches, name) {
      return { name: name, architectures: arches };
    });
   },

  // Processes the documentation provided in Package.describe. Returns an object
  // with the following keys:
  //   - path: full filepath to the Readme file
  //   - excerpt: the subsection between the first and second heading of the
  //     Readme, to be used as a longform package description.
  //   - hash: hash of the full text of this Readme, or "" if the Readme is
  //     blank.
  //
  // Returns null if the documentation is marked as null, or throws a
  // buildmessage error if the documentation could not be read.
  //
  // This function reads and performs string operations on a (potentially) long
  // file. We do not call it unless we actually need this information.
  processReadme: function () {
    var self = this;
    buildmessage.assertInJob();
    if (! self.metadata.documentation) {
      return null;
    }

    // To ensure atomicity, we want to copy the README to a temporary file.
    var ret = {};
    ret.path =
      files.pathJoin(self.sourceRoot, self.metadata.documentation);
    // Read in the text of the Readme.
    try {
      var fullReadme = files.readFile(ret.path);
    } catch (err) {
      var errorMessage = "";
      if (err.code === "ENOENT") {
        // This is the most likely and common case, especially when we are
        // inferring the docs as a default value.
        errorMessage = "Documentation not found: " + self.metadata.documentation;
      } else {
        // This is weird, and we don't usually protect the user from errors like
        // this, but maybe we should.
        errorMessage =
          "Documentation couldn't be read: " + self.metadata.documentation + " ";
        errorMessage += "(Error: " + err.message + ")";
      }

      // The user might not understand that we are automatically inferring
      // README.md as the docs! If they want to avoid pushing anything, explain
      // how to do that.
      if (! self.docsExplicitlyProvided) {
        errorMessage += "\n" +
          "If you don't want to publish any documentation, " +
          "please set 'documentation: null' in Package.describe";
      }
      buildmessage.error(errorMessage);
      // Recover by returning null
      return null;
    }

    var text = fullReadme.toString();
    return {
      contents: text,
      hash: utils.sha256(text),
      excerpt: getExcerptFromReadme(text)
    };
  },

  // If dependencies aren't consistent across unibuilds, return false and
  // also log a buildmessage error if inside a buildmessage job. Else
  // return true.
  // XXX: Check that this is used when refactoring is done.
  _checkCrossUnibuildVersionConstraints: function () {
    var self = this;
    return !! self._computeDependencyMetadata({ logError: true });
  },

  // Compute the return value for getDependencyMetadata, or return
  // null if there is a dependency that doesn't have the same
  // constraint across all unibuilds (and, if logError is true, log a
  // buildmessage error).
  //
  // This *DOES* include isobuild:* pseudo-packages!
  //
  // For options, see getDependencyMetadata.
  _computeDependencyMetadata: function (options) {
    var self = this;
    options = options || {};

    var dependencies = {};
    var allConstraints = {}; // for error reporting. package name to array
    var failed = false;

    _.each(self.architectures, function (arch) {
      // We need to iterate over both uses and implies, since implied packages
      // also constitute dependencies.
      var processUse = function (implied, use) {
        // We can't really have a weak implies (what does that even mean?) but
        // we check for that elsewhere.
        if ((use.weak && options.skipWeak) ||
            (use.unordered && options.skipUnordered))
          return;

        if (!_.has(dependencies, use.package)) {
          dependencies[use.package] = {
            constraint: null,
            references: []
          };
          allConstraints[use.package] = [];
        }
        var d = dependencies[use.package];

        if (use.constraint) {
          allConstraints[use.package].push(use.constraint);

          if (d.constraint === null) {
            d.constraint = use.constraint;
          } else if (d.constraint !== use.constraint) {
            failed = true;
          }
        }

        var reference = {
          arch: archinfo.withoutSpecificOs(arch.arch)
        };
        if (use.weak) {
          reference.weak = true;
        }
        if (use.unordered) {
          reference.unordered = true;
        }
        if (implied) {
          reference.implied = true;
        }
        d.references.push(reference);
      };
      _.each(arch.uses, _.partial(processUse, false));
      _.each(arch.implies, _.partial(processUse, true));
    });

    _.each(self.pluginInfo, function (info) {
      _.each(info.use, function (spec) {
        var parsedSpec = splitConstraint(spec);
        if (!_.has(dependencies, parsedSpec.package)) {
          dependencies[parsedSpec.package] = {
            constraint: null,
            references: []
          };
          allConstraints[parsedSpec.package] = [];
        }
        var d = dependencies[parsedSpec.package];

        if (parsedSpec.constraint) {
          allConstraints[parsedSpec.package].push(parsedSpec.constraint);
          if (d.constraint === null) {
            d.constraint = parsedSpec.constraint;
          } else if (d.constraint !== parsedSpec.constraint) {
            failed = true;
          }
        }
        d.references.push({arch: 'plugin'});
      });
    });

    if (failed && options.logError) {
      _.each(allConstraints, function (constraints, name) {
        constraints = _.uniq(constraints);
        if (constraints.length > 1) {
          buildmessage.error(
            "The version constraint for a dependency must be the same " +
              "at every place it is mentioned in a package. " +
              "'" + name + "' is constrained both as "  +
              constraints.join(' and ') + ". Change them to match.");
          // recover by returning false (the caller had better detect
          // this and use its own recovery logic)
        }
      });
    }

    return failed ? null : dependencies;
  },

  displayName() {
    return this.name === null ? 'the app' : this.name;
  }
});

module.exports = PackageSource;


// XXX: When we update this, see if it fixes this Github issue:

// https://github.com/jgm/CommonMark/issues/276 . If it does, remove the

// XXX We ought to be able to get this from the copy in js-analyze rather

keep keep keep keep replace replace keep

  <title>jsparser</title>

  {{> page}}

<template name="page">

      This is a demo of the new jsparse package.
      Edit code on the left.
      Click on an outlined box on the right
      to select it in the code.  The full spec is supported.

{{!
whitespace is significant here; browser swallows initial
    newline in textarea
}}<textarea id="inputtextarea">
{{input}}</textarea>

      {{#each outputTypes}}
      {{name}}
      {{/each}}

      {{output}}

</template>


import {WatchSet, readAndWatchFile, sha1} from './watch.js';
import files from './files.js';
import NpmDiscards from './npm-discards.js';
import {Profile} from './profile.js';
import {startsWith} from './utils.js';

const ENABLE_IN_PLACE_BUILDER_REPLACEMENT =
  ! process.env.METEOR_DISABLE_BUILDER_IN_PLACE;

// Builder encapsulates much of the file-handling logic need to create
// "bundles" (directory trees such as site archives, programs, or
// packages). It can create a temporary directory in which to build
// the bundle, moving the bundle atomically into place when and if the
// build successfully completes; sanitize and generate unique
// filenames; and track dependencies (files that should be watched for
// changes when developing interactively).
//
// Options:
//  - outputPath: Required. Path to the directory that will hold the
//    bundle when building is complete. It should not exist. Its
//    parents will be created if necessary.
// - previousBuilder: Optional. An in-memory instance of Builder left
// from the previous iteration. It is assumed that the previous builder
// has completed its job successfully and its files are stored on the
// file system in the exact layout as described in its usedAsFile data
// structure; and the hashes of the contents correspond to the
// writtenHashes data strcture.
export default class Builder {
  constructor({outputPath, previousBuilder}) {
    this.outputPath = outputPath;

    // Paths already written to. Map from canonicalized relPath (no
    // trailing slash) to true for a file, or false for a directory.
    this.usedAsFile = { '': false, '.': false };
    this.previousUsedAsFile = {};

    this.writtenHashes = {};
    this.previousWrittenHashes = {};

    // foo/bar => foo/.build1234.bar
    // Should we include a random number? The advantage is that multiple
    // builds can run in parallel. The disadvantage is that stale build
    // files hang around forever. For now, go with the former.
    const nonce = Math.floor(Math.random() * 999999);
    this.buildPath = files.pathJoin(files.pathDirname(this.outputPath),
                                    '.build' + nonce + "." +
                                    files.pathBasename(this.outputPath));

    let resetBuildPath = true;

    // If we have a previous builder and we are allowed to re-use it,
    // let's keep all the older files on the file-system and replace
    // only outdated ones + write the new files in the same path
    if (previousBuilder && ENABLE_IN_PLACE_BUILDER_REPLACEMENT) {
      if (previousBuilder.outputPath !== outputPath) {
        throw new Error(
          `previousBuilder option can only be set to a builder with the same output path.
Previous builder: ${previousBuilder.outputPath}, this builder: ${outputPath}`
        );
      }

      if (files.exists(previousBuilder.outputPath)) {
        // write files in-place in the output directory of the previous builder
        this.buildPath = previousBuilder.outputPath;

        this.previousWrittenHashes = previousBuilder.writtenHashes;
        this.previousUsedAsFile = previousBuilder.usedAsFile;

        resetBuildPath = false;
      } else {
        resetBuildPath = true;
      }
    }

    // Build the output from scratch
    if (resetBuildPath) {
      files.rm_recursive(this.buildPath);
      files.mkdir_p(this.buildPath, 0o755);
    }

    this.watchSet = new WatchSet();

    // XXX cleaner error handling. don't make the humans read an
    // exception (and, make suitable for use in automated systems)
  }

  // Like mkdir_p, but records in self.usedAsFile that we have created
  // the directories, and takes a path relative to the bundle
  // root. Throws an exception on failure.
  _ensureDirectory(relPath) {
    const parts = files.pathNormalize(relPath).split(files.pathSep);
    if (parts.length > 1 && parts[parts.length - 1] === '')
      parts.pop(); // remove trailing slash

    const partsSoFar = [];
    parts.forEach(part => {
      partsSoFar.push(part);
      const partial = partsSoFar.join(files.pathSep);
      if (! (partial in this.usedAsFile)) {
        let needToMkdir = true;
        if (partial in this.previousUsedAsFile) {
          if (this.previousUsedAsFile[partial]) {
            // was previously used as file, delete it, create a directory
            files.unlink(partial);
          } else {
            // is already a directory
            needToMkdir = false;
          }
        }

        if (needToMkdir) {
          // It's new -- create it
          files.mkdir(files.pathJoin(this.buildPath, partial), 0o755);
        }
        this.usedAsFile[partial] = false;
      } else if (this.usedAsFile[partial]) {
        // Already exists and is a file. Oops.
        throw new Error(`tried to make ${relPath} a directory but ${partial} is already a file`);
      } else {
        // Already exists and is a directory
      }
    });
  }

  // isDirectory defaults to false
  _sanitize(relPath, isDirectory) {
    const parts = relPath.split(files.pathSep);
    const partsOut = [];
    for (let i = 0; i < parts.length; i++) {
      let part = parts[i];
      const shouldBeFile = (i === parts.length - 1) && ! isDirectory;
      const mustBeUnique = (i === parts.length - 1);

      // Basic sanitization
      if (part.match(/^\.+$/))
        throw new Error(`Path contains forbidden segment '${part}'`);

      part = part.replace(/[^a-zA-Z0-9._\:-]/g, '');

      // If at last component, pull extension (if any) off of part
      let ext = '';
      if (shouldBeFile) {
        const split = part.split('.');
        if (split.length > 1)
          ext = "." + split.pop();
        part = split.join('.');
      }

      // Make sure it's sufficiently unique
      let suffix = '';
      while (true) {
        const candidate = files.pathJoin(partsOut.join(files.pathSep), part + suffix + ext);
        if (candidate.length) {
          // If we've never heard of this, then it's unique enough.
          if (!(candidate in this.usedAsFile))
            break;
          // If we want this bit to be a directory, and we don't need it to be
          // unique (ie, it isn't the very last bit), and it's currently a
          // directory, then that's OK.
          if (!(mustBeUnique || this.usedAsFile[candidate]))
            break;
          // OK, either we want it to be unique and it already exists; or it is
          // currently a file (and we want it to be either a different file or a
          // directory).  Try a new suffix.
        }

        suffix++; // first increment will do '' -> 1
      }

      partsOut.push(part + suffix + ext);
    }

    return partsOut.join(files.pathSep);
  }

  // Write either a buffer or the contents of a file to `relPath` (a
  // path to a file relative to the bundle root), creating it (and any
  // enclosing directories) if it doesn't exist yet. Exactly one of
  // `data` and or `file` must be passed.
  //
  // Options:
  // - data: a Buffer to write to relPath. Overrides `file`.
  // - file: a filename to write to relPath, as a string.
  // - sanitize: if true, then all components of the path are stripped
  //   of any potentially troubling characters, an exception is thrown
  //   if any path segments consist entirely of dots (eg, '..'), and
  //   if there is a file in the bundle with the same relPath, then
  //   the path is changed by adding a numeric suffix.
  // - hash: a sha1 string used to determine if the contents of the
  //   new file written is not cached.
  // - executable: if true, mark the file as executable.
  // - symlink: if set to a string, create a symlink to its value
  //
  // Returns the final canonicalize relPath that was written to.
  //
  // If `file` is used then it will be added to the builder's WatchSet.
  write(relPath, {data, file, hash, sanitize, executable, symlink}) {
    // Ensure no trailing slash
    if (relPath.slice(-1) === files.pathSep)
      relPath = relPath.slice(0, -1);

    // In sanitize mode, ensure path does not contain segments like
    // '..', does not contain forbidden characters, and is unique.
    if (sanitize)
      relPath = this._sanitize(relPath);

    let getData = null;
    if (data) {
      if (! (data instanceof Buffer))
        throw new Error("data must be a Buffer");
      if (file)
        throw new Error("May only pass one of data and file, not both");
      getData = () => data;
    } else if (file) {
      // postpone reading the file into memory
      getData = () => readAndWatchFile(this.watchSet, files.pathResolve(file));
    } else if (! symlink) {
      throw new Error('Builder can not write without either data or a file path or a symlink path: ' + relPath);
    }

    this._ensureDirectory(files.pathDirname(relPath));
    const absPath = files.pathJoin(this.buildPath, relPath);

    if (symlink) {
      files.symlink(symlink, absPath);
    } else {
      hash = hash || sha1(getData());

      if (this.previousWrittenHashes[relPath] !== hash) {
        // Builder is used to create build products, which should be read-only;
        // users shouldn't be manually editing automatically generated files and
        // expecting the results to "stick".
        atomicallyRewriteFile(absPath, getData(), {
          mode: executable ? 0o555 : 0o444
        });
      }

      this.writtenHashes[relPath] = hash;
    }
    this.usedAsFile[relPath] = true;

    return relPath;
  }

  // Serialize `data` as JSON and write it to `relPath` (a path to a
  // file relative to the bundle root), creating parent directories as
  // necessary. Throw an exception if the file already exists.
  writeJson(relPath, data) {
    // Ensure no trailing slash
    if (relPath.slice(-1) === files.pathSep)
      relPath = relPath.slice(0, -1);

    this._ensureDirectory(files.pathDirname(relPath));
    const absPath = files.pathJoin(this.buildPath, relPath);

    atomicallyRewriteFile(
      absPath,
      new Buffer(JSON.stringify(data, null, 2), 'utf8'),
      {mode: 0o444});

    this.usedAsFile[relPath] = true;
  }

  // Add relPath to the list of "already taken" paths in the
  // bundle. This will cause write, when in sanitize mode, to never
  // pick this filename (and will prevent files that from being
  // written that would conflict with paths that we are expecting to
  // be directories). Calling this twice on the same relPath will
  // given an exception.
  //
  // Returns the *current* (temporary!) path to where the file or directory
  // lives. This is so you could use non-builder code to write into a reserved
  // directory.
  //
  // options:
  // - directory: set to true to reserve this relPath to be a
  //   directory rather than a file.
  reserve(relPath, {directory} = {}) {
    // Ensure no trailing slash
    if (relPath.slice(-1) === files.pathSep)
      relPath = relPath.slice(0, -1);

    const parts = relPath.split(files.pathSep);
    const partsSoFar = [];
    for (let i = 0; i < parts.length; i ++) {
      const part = parts[i];
      partsSoFar.push(part);
      const soFar = partsSoFar.join(files.pathSep);
      if (this.usedAsFile[soFar])
        throw new Error("Path reservation conflict: " + relPath);

      const shouldBeDirectory = (i < parts.length - 1) || directory;
      if (shouldBeDirectory) {
        if (! (soFar in this.usedAsFile)) {
          let needToMkdir = true;
          if (soFar in this.previousUsedAsFile) {
            if (this.previousUsedAsFile[soFar]) {
              files.unlink(soFar);
            } else {
              needToMkdir = false;
            }
          }
          if (needToMkdir) {
            files.mkdir(files.pathJoin(this.buildPath, soFar), 0o755);
          }
          this.usedAsFile[soFar] = false;
        }
      } else {
        this.usedAsFile[soFar] = true;
      }
    }

    // Return the path we reserved.
    return files.pathJoin(this.buildPath, relPath);
  }

  // Generate and reserve a unique name for a file based on `relPath`,
  // and return it. If `relPath` is available (there is no file with
  // that name currently existing or reserved, it doesn't contain
  // forbidden characters, a prefix of it is not already in use as a
  // file rather than a directory) then the return value will be
  // `relPath`. Otherwise relPath will be modified to get the return
  // value, say by adding a numeric suffix to some path components
  // (preserving the file extension however) and deleting forbidden
  // characters. Throws an exception if relPath contains any segments
  // that are all dots (eg, '..').
  //
  // options:
  //
  // - directory: generate (and reserve) a name for a directory,
  //   rather than a file.
  generateFilename(relPath, {directory} = {}) {
    relPath = this._sanitize(relPath, directory);
    this.reserve(relPath, { directory });
    return relPath;
  }

  // Convenience wrapper around generateFilename and write.
  //
  // (Note that in the object returned by builder.enter, this method
  // is patched through directly rather than rewriting its inputs and
  // outputs. This is only valid because it does nothing with its inputs
  // and outputs other than send pass them to other methods.)
  writeToGeneratedFilename(relPath, writeOptions) {
    const generated = this.generateFilename(relPath);
    this.write(generated, writeOptions);
    return generated;
  }

  // Recursively copy a directory and all of its contents into the
  // bundle. But if the symlink option was passed to the Builder
  // constructor, then make a symlink instead, if possible.
  //
  // Unlike with files.cp_r, if a symlink is found, it is copied as a symlink.
  //
  // This does NOT add anything to the WatchSet.
  //
  // Options:
  // - from: source path on local disk to copy from
  // - to: relative path to a directory in the bundle that will
  //   receive the files
  // - ignore: array of regexps of filenames (that is, basenames) to
  //   ignore (they may still be visible in the output bundle if
  //   symlinks are being used).  Like with WatchSets, they match against
  //   entries that end with a slash if it's a directory.
  // - specificFiles: just copy these paths (specified as relative to 'to').
  // - symlink: true if the directory should be symlinked instead of copying
  copyDirectory({from, to, ignore, specificFiles, symlink, npmDiscards}) {
    if (to.slice(-1) === files.pathSep)
      to = to.slice(0, -1);

    const absPathTo = files.pathJoin(this.buildPath, to);
    if (symlink) {
      if (specificFiles) {
        throw new Error("can't copy only specific paths with a single symlink");
      }

      if (this.usedAsFile[to]) {
        throw new Error("tried to copy a directory onto " + to +
                        " but it is is already a file");
      }

      let canSymlink = true;
      // Symlinks don't work exactly the same way on Windows, and furthermore
      // they request Admin permissions to set.
      if (process.platform === 'win32') {
        canSymlink = false;
      } else if (to in this.usedAsFile) {
        // It's already here and is a directory, maybe because of a call to
        // reserve with {directory: true}. If it's an empty directory, this is
        // salvageable. The directory should exist, because all code paths which
        // set usedAsFile to false create the directory.
        //
        // XXX This is somewhat broken: what if the reason we're in
        // self.usedAsFile is because an immediate child of ours was reserved as
        // a file but not actually written yet?
        const children = files.readdir(absPathTo);
        if (Object.keys(children).length === 0) {
          files.rmdir(absPathTo);
        } else {
          canSymlink = false;
        }
      }

      if (canSymlink) {
        this._ensureDirectory(files.pathDirname(to));
        files.symlink(files.pathResolve(from), absPathTo);
        return;
      }
    }

    ignore = ignore || [];
    let specificPaths = null;
    if (specificFiles) {
      specificPaths = {};
      specificFiles.forEach(f => {
        while (f !== '.') {
          specificPaths[files.pathJoin(to, f)] = true;
          f = files.pathDirname(f);
        }
      });
    }

    let walk = (absFrom, relTo) => {
      this._ensureDirectory(relTo);

      files.readdir(absFrom).forEach(item => {
        const thisAbsFrom = files.pathResolve(absFrom, item);
        const thisRelTo = files.pathJoin(relTo, item);

        if (specificPaths && !(thisRelTo in specificPaths)) {
          return;
        }

        const fileStatus = files.lstat(thisAbsFrom);

        let itemForMatch = item;
        const isDirectory = fileStatus.isDirectory();
        if (isDirectory) {
          itemForMatch += '/';
        }

        // skip excluded files
        if (ignore.some(pattern => itemForMatch.match(pattern))) return;

        if (npmDiscards instanceof NpmDiscards &&
            npmDiscards.shouldDiscard(thisAbsFrom, isDirectory)) {
          return;
        }

        if (isDirectory) {
          walk(thisAbsFrom, thisRelTo);
        } else if (fileStatus.isSymbolicLink()) {
          files.symlink(files.readlink(thisAbsFrom),
                         files.pathResolve(this.buildPath, thisRelTo));
          // A symlink counts as a file, as far as "can you put something under
          // it" goes.
          this.usedAsFile[thisRelTo] = true;
        } else {
          // XXX can't really optimize this copying without reading
          // the file into memory to calculate the hash.
          files.copyFile(thisAbsFrom,
                         files.pathResolve(this.buildPath, thisRelTo),
                         fileStatus.mode);
          this.usedAsFile[thisRelTo] = true;
        }
      });
    };

    walk(from, to);
  }

  // Returns a new Builder-compatible object that works just like a
  // Builder, but interprets all paths relative to 'relPath', a path
  // relative to the bundle root which should not start with a '/'.
  //
  // The sub-builder returned does not have all Builder methods (for
  // example, complete() wouldn't make sense) and you should not rely
  // on it being instanceof Builder.
  enter(relPath) {
    const methods = ["write", "writeJson", "reserve", "generateFilename",
                   "copyDirectory", "enter"];
    const subBuilder = {};
    const relPathWithSep = relPath + files.pathSep;

    methods.forEach(method => {
      subBuilder[method] = (...args) => {
        if (method !== "copyDirectory") {
          // Normal method (relPath as first argument)
          args[0] = files.pathJoin(relPath, args[0]);
        } else {
          // with copyDirectory the path we have to fix up is inside
          // an options hash
          args[0].to = files.pathJoin(relPath, args[0].to);
        }

        let ret = this[method](...args);

        if (method === "generateFilename") {
          // fix up the returned path to be relative to the
          // sub-bundle, not the parent bundle
          if (ret.substr(0, 1) === '/')
            ret = ret.substr(1);
          if (ret.substr(0, relPathWithSep.length) !== relPathWithSep)
            throw new Error("generateFilename returned path outside of " +
                            "sub-bundle?");
          ret = ret.substr(relPathWithSep.length);
        }

        return ret;
      };
    });

    // Methods that don't have to fix up arguments or return values, because
    // they are implemented purely in terms of other methods which do.
    const passThroughMethods = ["writeToGeneratedFilename"];
    passThroughMethods.forEach(method => {
      subBuilder[method] = this[method];
    });

    return subBuilder;
  }

  // Move the completed bundle into its final location (outputPath)
  complete() {
    if (this.previousUsedAsFile) {
      // delete files and folders left-over from previous runs and not
      // re-used in this run
      const removed = {};
      const paths = Object.keys(this.previousUsedAsFile);
      paths.forEach((path) => {
        // if the same path was re-used, leave it
        if (this.usedAsFile.hasOwnProperty(path)) { return; }

        // otherwise, remove it as it is no longer needed

        // skip if already deleted
        if (removed.hasOwnProperty(path)) { return; }

        const absPath = files.pathJoin(this.buildPath, path);
        if (this.previousUsedAsFile[path]) {
          // file
          files.unlink(absPath);
          removed[path] = true;
        } else {
          // directory
          files.rm_recursive(absPath);

          // mark all sub-paths as removed, too
          paths.forEach((anotherPath) => {
            if (startsWith(anotherPath, path + '/')) {
              removed[anotherPath] = true;
            }
          });
        }
      });
    }

    // XXX Alternatively, we could just keep buildPath around, and make
    // outputPath be a symlink pointing to it. This doesn't work for the NPM use
    // case of renameDirAlmostAtomically since that one is constructing files to
    // be checked in to version control, but here we could get away with it.
    if (this.buildPath !== this.outputPath) {
      files.renameDirAlmostAtomically(this.buildPath, this.outputPath);
    }
  }

  // Delete the partially-completed bundle. Do not disturb outputPath.
  abort() {
    files.rm_recursive(this.buildPath);
  }

  // Returns a WatchSet representing all files that were read from disk by the
  // builder.
  getWatchSet() {
    return this.watchSet;
  }
}

function atomicallyRewriteFile(path, data, options) {
  let stat = null;
  try {
    stat = files.stat(path);
  } catch (e) {
    if (e.code !== 'ENOENT') {
      throw e;
    }
  }

  if (! stat) {
    files.writeFile(path, data, options);
  } else if (stat.isDirectory()) {
    files.rm_recursive(path);
  } else {
    // create a different file with a random name and then rename over atomically
    const rname = '.builder-tmp-file.' + Math.floor(Math.random() * 999999);
    const rpath = files.pathJoin(files.pathDirname(path), rname);
    files.writeFile(rpath, data, options);
    files.rename(rpath, path);
  }
}

// Wrap slow methods into Profiler calls
const slowBuilderMethods = [
  '_ensureDirectory', 'write', 'enter', 'copyDirectory', 'enter', 'complete'
];

slowBuilderMethods.forEach(method => {
  Builder.prototype[method] =
    Profile(`Builder#${method}`, Builder.prototype[method]);
});


// 2.4.0 (more or less, the package.json change isn't committed) plus our PR

#!/bin/bash

BUNDLE_VERSION=0.5.0

# OS Check. Put here because here is where we download the precompiled
# bundles that are arch specific.
UNAME=$(uname)
if [ "$UNAME" != "Linux" -a "$UNAME" != "Darwin" ] ; then
    echo "Sorry, this OS is not supported."
    exit 1
fi

if [ "$UNAME" = "Darwin" ] ; then
    if [ "i386" != "$(uname -p)" -o "1" != "$(sysctl -n hw.cpu64bit_capable 2>/dev/null || echo 0)" ] ; then

        # Can't just test uname -m = x86_64, because Snow Leopard can
        # return other values.
        echo "Only 64-bit Intel processors are supported at this time."
        exit 1
    fi
    ARCH="x86_64"
elif [ "$UNAME" = "Linux" ] ; then
    ARCH="$(uname -m)"
    if [ "$ARCH" != "i686" -a "$ARCH" != "x86_64" ] ; then
        echo "Unsupported architecture: $ARCH"
        echo "Meteor only supports i686 and x86_64 for now."
        exit 1
    fi
fi
PLATFORM="${UNAME}_${ARCH}"

# Find the script dir, following symlinks. Note that symlink can be relative or
# absolute. Too bad 'readlink -f' and 'realpath' (the command-line program) are
# not portable.  We don't stress about infinite loops or bad links, because the
# OS has already resolved this symlink chain once in order to actually run the
# shell script.
ORIG_DIR="$(pwd)"
SCRIPT="$0"
while true; do
  # The symlink might be relative, so we have to actually cd to the right place
  # each time in order to resolve it.
  cd "$(dirname "$SCRIPT")"
  if [ ! -L "$(basename "$SCRIPT")" ]; then
    SCRIPT_DIR="$(pwd -P)"
    break
  fi
  SCRIPT="$(readlink "$(basename "$SCRIPT")")"
done
cd "$ORIG_DIR"

function install_dev_bundle {
    set -e
    trap "echo Failed to install dependency kit." EXIT

    TARBALL="dev_bundle_${PLATFORM}_${BUNDLE_VERSION}.tar.gz"
    BUNDLE_TMPDIR="$SCRIPT_DIR/dev_bundle.xxx"

    rm -rf "$BUNDLE_TMPDIR"
    mkdir "$BUNDLE_TMPDIR"

    # duplicated in scripts/windows/download-dev-bundle.ps1:
    DEV_BUNDLE_URL_ROOT="https://d3sqy0vbqsdhku.cloudfront.net/"
    # If you set $USE_TEST_DEV_BUNDLE_SERVER then we will download
    # dev bundles copied by copy-dev-bundle-from-jenkins.sh without --prod.
    # It still only does this if the version number has changed
    # (setting it won't cause it to automatically delete a prod dev bundle).
    if [ -n "$USE_TEST_DEV_BUNDLE_SERVER" ] ; then
        DEV_BUNDLE_URL_ROOT="https://s3.amazonaws.com/com.meteor.static/test/"
    fi

    if [ -f "$SCRIPT_DIR/$TARBALL" ] ; then
        echo "Skipping download and installing kit from $SCRIPT_DIR/$TARBALL" >&2
        tar -xzf "$SCRIPT_DIR/$TARBALL" -C "$BUNDLE_TMPDIR"
    elif [ -n "$SAVE_DEV_BUNDLE_TARBALL" ] ; then
        # URL duplicated in tools/server/target.sh.in
        curl -# "$DEV_BUNDLE_URL_ROOT$TARBALL" >"$SCRIPT_DIR/$TARBALL"
        tar -xzf "$SCRIPT_DIR/$TARBALL" -C "$BUNDLE_TMPDIR"
    else
        curl -# "$DEV_BUNDLE_URL_ROOT$TARBALL" | tar -xzf - -C "$BUNDLE_TMPDIR"
    fi

    test -x "${BUNDLE_TMPDIR}/bin/node" # bomb out if it didn't work, eg no net

    # Delete old dev bundle and rename the new one on top of it.
    rm -rf "$SCRIPT_DIR/dev_bundle"
    mv "$BUNDLE_TMPDIR" "$SCRIPT_DIR/dev_bundle"

    echo "Installed dependency kit v${BUNDLE_VERSION} in dev_bundle." >&2
    echo >&2

    trap - EXIT
    set +e
}

if [ -d "$SCRIPT_DIR/.git" ] || [ -f "$SCRIPT_DIR/.git" ]; then
    # In a checkout.
    if [ ! -d "$SCRIPT_DIR/dev_bundle" ] ; then
        echo "It's the first time you've run Meteor from a git checkout." >&2
        echo "I will download a kit containing all of Meteor's dependencies." >&2
        install_dev_bundle
    elif [ ! -f "$SCRIPT_DIR/dev_bundle/.bundle_version.txt" ] ||
        grep -qvx "$BUNDLE_VERSION" "$SCRIPT_DIR/dev_bundle/.bundle_version.txt" ; then
        echo "Your dependency kit is out of date. I will download the new one." >&2
        install_dev_bundle
    fi

    export BABEL_CACHE_DIR="$SCRIPT_DIR/.babel-cache"
fi

DEV_BUNDLE="$SCRIPT_DIR/dev_bundle"
METEOR="$SCRIPT_DIR/tools/main-transpile-wrapper.js"

# Bump our file descriptor ulimit as high as it will go. This is a
# temporary workaround for dependancy watching holding open too many
# files: https://app.asana.com/0/364581412985/472479912325
if [ "$(ulimit -n)" != "unlimited" ] ; then
    ulimit -n 16384 > /dev/null 2>&1 || \
    ulimit -n 8192 > /dev/null 2>&1 || \
    ulimit -n 4096 > /dev/null 2>&1 || \
    ulimit -n 2048 > /dev/null 2>&1 || \
    ulimit -n 1024 > /dev/null 2>&1 || \
    ulimit -n 512 > /dev/null 2>&1
fi

# We used to set $NODE_PATH here to include the node_modules from the dev
# bundle, but now we just get them from the symlink at tools/node_modules. This
# is better because node_modules directories found via the ancestor walk from
# the script take precedence over $NODE_PATH; it used to be that users would
# screw up their meteor installs by have a ~/node_modules

exec "$DEV_BUNDLE/bin/node" "$METEOR" "$@"