// These commands deal with aggregating local package data with the information // contained in the Meteor Package Server. They also deal with presenting this // to the user in various human or machine-readable ways. var _ = require('underscore'); var archinfo = require('./archinfo.js'); var buildmessage = require('./buildmessage.js'); var catalog = require('./catalog.js'); var Console = require("./console.js").Console; var files = require('./files.js'); var isopackets = require('./isopackets.js'); var main = require('./main.js'); var packageVersionParser = require('./package-version-parser.js'); var projectContextModule = require('./project-context.js'); var utils = require('./utils.js'); var compiler = require('./compiler.js'); // We want these queries to be relatively fast, so we will only refresh the // catalog if it is > 15 minutes old var DEFAULT_MAX_AGE_MS = 15 * 60 * 1000; // Maximum number of recent versions of a package or a release that we should // return to the user, unless a more complete mode is requested. var MAX_RECENT_VERSIONS = 5; // XXX: Remove this if/when we do a Troposphere migration to backfill release // version publication times. // Estimate the publication date for a release. Since we have failed to keep // track of publication times of release versions in the past, we will try to // guess that the release was published at the same time as the tool. var getReleaseVersionPublishedOn = function (versionRecord) { if (versionRecord.published) { return new Date(versionRecord.published); } // We don't know when the release was published. Luckily, since there is no // way to use the tool outside of a release, and we always change the tool // between releases, it is a good bet that the release was published on the // same day as the tool. var toolPackage = versionRecord.tool.split('@'); var toolName = toolPackage[0]; var toolVersion = toolPackage[1]; var toolRecord = catalog.official.getVersion(toolName, toolVersion); if (! toolRecord || ! toolRecord.published) return null; return new Date(toolRecord.published); }; // Processes information about the versions that we hid. Returns a brief // human-friendly string listing the reasons why some versions of the package // were not shown. var formatHiddenVersions = function (hiddenVersions, oldestShownVersion) { // An array of strings, listing the reasons why some versions were hidden. var reasons = []; // Use our information about hidden versions to figure what reasons we // actually want to return to the user. if (! oldestShownVersion) { // We did not show any versions, so presumably all existing versions of // this package are either unmigrated or pre-release versions. if (hiddenVersions.lastUnmigrated) { reasons.push("unmigrated"); } if (hiddenVersions.lastPreRelease) { reasons.push("pre-release"); } } else { // If the oldest version on record is older than the oldest shown // version, then it was hidden due to MAX_RECENT_VERSION number. (It // might also be hidden because it is a pre-release or unmigrated, but // age takes priority). if (packageVersionParser.lessThan( hiddenVersions.oldestVersion, oldestShownVersion)) { reasons.push("older"); } // If the latest unmigrated/pre-release version is older than the oldest // version that we are showing, then we don't care about it. If it is // younger, we need to tell the user. // // It is certainly possible that, even though a pre-release version is older // than the oldest version that we are showing, but under the limit for the // MAX_RECENT_VERSIONS. So, in that case, we are eliding that version // because it is a pre-release, not because of age. It is still, // technically, an 'older' version though, and that explanation is more // intuitive. if (hiddenVersions.lastPreRelease && packageVersionParser.lessThan( oldestShownVersion, hiddenVersions.lastPreRelease)) { reasons.push("pre-release"); } if (hiddenVersions.lastUnmigrated && packageVersionParser.lessThan( oldestShownVersion, hiddenVersions.lastUnmigrated)) { reasons.push("unmigrated"); } } // Now, we will aggregate the reasons into a human-readable string. if (reasons.length === 1) { return reasons[0]; } else if (reasons.length === 2) { // There is no oxford comma if only listing two objects return reasons[0] + " and " + reasons[1]; } else if (reasons.length > 2) { return reasons.slice(0, -1).join(", ") + ", and " + _.last(reasons); } else { // Did we not figure out anything to write? Did something else go wrong? // This should never happen, but if it does, recover by omitting // information. return "Some"; } }; // Converts an object to an EJSON string with the right spacing. var formatEJSON = function (data) { var EJSON = isopackets.load('ejson').ejson.EJSON; return EJSON.stringify(data, { indent: true }) + "\n"; }; // Takes in a string and pads it with whitespace to the length of the longest // possible date string. var padLongformDate = function (dateStr) { var numSpaces = utils.maxDateLength - dateStr.length; return dateStr + Array(numSpaces + 1).join(' '); }; // In order to get access to local package data, we need to create a local // package catalog. The best way to do that is to create a temporary // ProjectContext and let it handle catalog initialization. When we do, we need // to make sure that it is aware of all the local packages that we might care // about. // // This function returns such a ProjectContext, and takes in the following // options: // - appDir: If we are running in the context of an app, this will contain the // root of the app. We want to make sure to grab the data from the app's // local packages. // - packageDir: If we are running in a package directory, this will contain // the source root of that package. If we are running from inside a package, // we want that package to show up in our results. var getTempContext = function (options) { var projectContext; // If we are running in an app, we will use it to create a // (mostly immutable) projectContext. if (options.appDir) { projectContext = new projectContextModule.ProjectContext({ projectDir: options.appDir }); } else { // We're not in an app, so we will create a temporary app and use it to load // the local catalog. If a local packageDir exists, include it manually. var currentPackageDir = options.packageDir ? [options.packageDir] : []; var tempProjectDir = files.mkdtemp('meteor-show'); projectContext = new projectContextModule.ProjectContext({ projectDir: tempProjectDir, explicitlyAddedLocalPackageDirs: currentPackageDir }); } // It is possible that we can't process package.js files in our local packages // and have to exit early. This is unfortunate, but we can't search local // packages if we can't read them. If this turns out to be a frequent problem, // we can give a warning, instead of failing in the future. For now, we want // to err on the side of consistency. main.captureAndExit("=> Errors while reading local packages:", function () { projectContext.initializeCatalog(); }); return projectContext; }; // Print an error message if the user asks about an unknown item. var itemNotFound = function (item) { Console.error(item + ": not found"); utils.explainIfRefreshFailed(); return 1; }; // This is a base class for storing package fields that require some processing // to store and display correctly. // // Do NOT initialize this class by itself -- use one of the classes that // inherits from it. var BasePkgDatum = function () { var self = this; self.data = null; }; _.extend(BasePkgDatum.prototype, { // Throws if data has not been initialized. _checkInitialized: function () { var self = this; if (self.data === null) { throw new Error("do not use the BasePkgDatum class by itself"); } }, // Returns true if this class does not contain any exports. isEmpty : function () { var self = this; self._checkInitialized(); return _.isEmpty(self.data); }, // Get exports as a raw object. getObject : function () { var self = this; self._checkInitialized(); return self.data; }, getConsoleStr : function () { var self = this; self._checkInitialized(); return ""; } }); // This class stores exports from a given package. // // Stores exports for a given package and returns them to the caller in a given // format. Takes in the raw exports from the package. var PkgExports = function (pkgExports) { var self = this; // Process and save the export data. self.data = _.map(pkgExports, function (exp) { var arches = exp.architectures; // Replace 'os' (what we store) with 'server' (what you would put in a // package.js file). That's more user friendly, and avoids confusing this // with different OS arches used in binary packages. if ( _.indexOf(arches, "os") !== -1) { arches = _.without(arches, "os"); arches.push("server"); } // Sort architectures alphabetically. arches.sort(); return { name: exp.name, architectures: arches }; }); // Sort exports alphabetically by name. self.data = _.sortBy(self.data, "name"); }; // Extend BasePkgDatum. PkgExports.prototype = new BasePkgDatum(); _.extend(PkgExports.prototype, { // Convert package exports into a pretty, Console non-wrappable string. If an // export is only declared for certain architectures, mentions those // architectures in a user-friendly format. getConsoleStr: function () { var self = this; var strExports = _.map(self.data, function (exp) { // If this export is valid for all architectures, don't specify // architectures here. if (exp.architectures.length === compiler.ALL_ARCHES.length) return exp.name; // Don't split descriptions of individual pkgExports between lines. return Console.noWrap( exp.name + " (" + exp.architectures.join(", ") + ")"); }); return strExports.join(", "); } }); // This class stores implies from a given package. // // Stores implies for a given package and returns them to the caller in a given // format. Takes in the dependencies from the package. var PkgImplies = function (pkgDeps) { var self = this; self.data = []; // Go through all the package dependencies. If a dependency has any implied // references, add it to the list. _.each(pkgDeps, function (ref, name) { var architectures = []; // We want to select the references that are implied (instead of just used) // and save their architectures. Also, we want to replace 'os' with // 'server', as with exports. _.each(ref.references, function (r) { if (! r.implied) return; var archName = (r.arch === "os") ? "server" : r.arch; architectures.push(archName); }); // Sort architecures alphabetically. architectures.sort(); if (! _.isEmpty(architectures)) { self.data.push({ name: name, architectures: architectures }); } }); // Sort by name. self.data = _.sortBy(self.data, "name"); }; // Extend BasePkgDatum. PkgImplies.prototype = new BasePkgDatum(); _.extend(PkgImplies.prototype, { // Convert package exports into a pretty, Console non-wrappable string. If an // export is only declared for certain architectures, mentions those // architectures in a user-friendly format. getConsoleStr: function () { var self = this; var strImplies = _.map(self.data, function (ref) { // If an imply is valid for all architectures, don't specify it here. if (ref["architectures"].length === compiler.ALL_ARCHES.length) return ref.name; // Don't split descriptions of individual implies between lines. return Console.noWrap( ref.name + " (" + ref.architectures.join(", ") + ")"); }); return strImplies.join(", "); } }); // This class stores dependencies from a given package. // // Stores dependencies for a given package and returns them to the caller in a given // format. Takes in the raw dependencies from the package record. var PkgDependencies = function (pkgDeps) { var self = this; self.data = _.map( // The dependency on 'meteor' was almost certainly added automatically, by // Isobuild. Returning this to the user will only cause confusion. _.omit(pkgDeps, "meteor"), function (dep, depName) { // We will only consider this a weak dependency if all of its references // are marked as weak. var weak = _.every(dep.references, function (ref) { return !! ref.weak; }); return { name: depName, constraint: dep.constraint, weak: weak }; }); // Sort by name. self.data = _.sortBy(self.data, "name"); }; // Extend BasePkgDatum. PkgDependencies.prototype = new BasePkgDatum(); _.extend(PkgDependencies.prototype, { // Convert package exports into a pretty, Console non-wrappable string. If an // export is only declared for certain architectures, mentions those // architectures in a user-friendly format. getConsoleStr: function () { var self = this; var strDeps = _.map(self.data, function (dep) { var depString = dep.name; if (dep.constraint && dep.constraint !== null) { depString += "@" + dep.constraint; } if (dep.weak) { depString += " (weak dependency)"; } return Console.noWrap(depString); }); return strDeps.join("\n"); } }); // The two classes below collect and print relevant information about Meteor // packages and Meteor releases, respectively. Specifically, they query the // official catalog and, if applicable, relevant local sources. They also handle // the details of printing their data to the screen. // // A query class has: // - data: an object representing the data it has collected in response to the // - query. // - a print method, that take options as an argument and prints the results to // the terminal. // This class deals with information related to packages. To deal with local // packages, it has to interact with the projectContext. // // The constructor takes in the following options: // - metaRecord: (mandatory) the meta-record for this package from the Packages // collection. // - projectContext: (mandatory) a projectContext that we can use to look up // information on local packages. // - version: query for a specific version of this package. // - showArchitecturesOS: collect and process data on OS // architectures that are available for different versions of this package. // - showHiddenVersions: return information about all the versions of the // package, including pre-releases and un-migrate versions. // - showDependencies: return information information about // versions' dependencies. var PackageQuery = function (options) { var self = this; // This is the record in the packages collection. It contains things like // maintainers, and the package homepage. self.metaRecord = options.metaRecord; self.name = options.metaRecord.name; // This argument is required -- we use it to look up data. If it has not been // passed in, fail early. if (! options.projectContext) { throw Error("Missing required argument: projectContext"); } self.projectContext = options.projectContext; self.localCatalog = options.projectContext.localCatalog; // Processing per-version availability architectures & dependencies is // expensive, so we don't do it unless we are asked to. self.showArchitecturesOS = options.showArchitecturesOS; self.showDependencies = options.showDependencies; // We don't want to show pre-releases and un-migrated versions to the user // unless they explicitly ask us about it. self.showHiddenVersions = options.showHiddenVersions; // Collect the data for this package, including looking up any specific // package version that we care about. if (options.version) { var versionRecord = self._getVersionRecord(options.version); if (! versionRecord) { self.data = null; return; } self.data = versionRecord.local ? self._getLocalVersion(versionRecord) : self._getOfficialVersion(versionRecord); } else { self.data = self._collectPackageData(); } }; _.extend(PackageQuery.prototype, { // Find and return a version record for a given version. Mark the version // record as local, if it is a local version of the package. _getVersionRecord: function (version) { var self = this; // We allow local version to override remote versions in meteor show, so we // should start by checking if this is a local version first. var versionRecord = self.localCatalog.getLatestVersion(self.name); // If we asked for "local" as the version number, and found any local version // at all, we are done. if (version === "local") { return versionRecord && _.extend(versionRecord, { local: true }); } // We have a local record, and its version matches the version that we asked // for, so we are done. if (versionRecord && (versionRecord.version === version)) { return _.extend(versionRecord, { local: true }); } // If we haven't found a local record, or if the local record that we found // doesn't match the version that we asked for, then we have to go look in // the server catalog. versionRecord = catalog.official.getVersion(self.name, version); return versionRecord; }, // Print the query information to screen. // // options: // - ejson: Don't pretty-print the data. Print a machine-readable ejson // object. print: function (options) { var self = this; // If we are asking for an EJSON-style output, we will only print out the // relevant fields. if (options.ejson) { Console.rawInfo(formatEJSON( self.data.version ? self._generateVersionObject(self.data) : self._generatePackageObject(self.data))); return; } // Otherwise, display the information that we have. If we were asking about // a specific version, display that. Otherwise, display package metadata in // general. if (self.data.version) { self._displayVersion(self.data); return; } self._displayPackage(self.data); }, // Aggregates data about the package as a whole. Returns an object with the // following keys: // // - name: package name // - maintainers: an array of usernames of maintainers // - homepage: string homepage // - totalVersions: total number of versions that this package has, including // local and hidden versions. // - defaultVersion: a default version: use this version to look up // per-version information that is relevant to the package as a whole, such // as git, description,etc. // - versions: an array of objects representing versions of this package. _collectPackageData: function () { var self = this; var data = { name: self.metaRecord.name, maintainers: _.pluck(self.metaRecord.maintainers, "username"), homepage: self.metaRecord.homepage }; // Collect surface information about available versions, starting with the // versions available on the server. var serverVersionRecords = catalog.official.getSortedVersionRecords(self.name); var totalVersions = serverVersionRecords.length; // If we are not going to show hidden versions, then we shouldn't waste time // on them. Trim the serverVersionRecords array to only have the top // MAX_RECENT_VERSIONS migrated, official versions. if (! self.showHiddenVersions) { // We might have to hide some versions from the user. We want to explain // why we hid them. Here is how we are going to explain things -- any // versions older than the oldest version that we show, are hidden because // of age. If, in the covered time period, there are // unmigrated/pre-release versions, then we will mention those as well. // // Specifically, while we filter versions, we are going to memorize the // most recent version hidden for a specific reason. var lastUnmigrated = ""; var lastPreRelease = ""; var oldestVersion = serverVersionRecords[0] && serverVersionRecords[0].version; var filteredVersionRecords = _.filter(serverVersionRecords, function (vr) { if (vr.unmigrated) { lastUnmigrated = vr.version; return false; } if (vr.version.indexOf("-") !== -1) { lastPreRelease = vr.version; return false; } return true; }); serverVersionRecords = _.last(filteredVersionRecords, MAX_RECENT_VERSIONS); data["hiddenVersions"] = { oldestVersion: oldestVersion, lastUnmigrated: lastUnmigrated, lastPreRelease: lastPreRelease }; }; // Process the catalog records into our preferred format, and look up any // other per-version information that we might need. data["versions"] = _.map(serverVersionRecords, function (versionRecord) { return self._getOfficialVersion(versionRecord); }); // The local version doesn't count against the version limit. Look up relevant // information about the local version. var localVersion = self.localCatalog.getLatestVersion(self.name); var local; if (localVersion) { local = self._getLocalVersion(localVersion); data["versions"].push(local); totalVersions++; } // Record the total number of versions, including the ones we hid from the // user. data["totalVersions"] = totalVersions; // Some per-version information gets displayed with the rest of the package // information. We want to use the right version for that. (We don't want // to display data from unofficial or un-migrated versions just because they // are recent.) if (local) { data["defaultVersion"] = { version: "local", summary: local.summary, description: local.description, git: local.git, implies: local.implies, exports: local.exports }; } else { var mainlineRecord = catalog.official.getLatestMainlineVersion(self.name); if (mainlineRecord) { var pkgExports = new PkgExports(mainlineRecord.exports); var implies = new PkgImplies(mainlineRecord.dependencies); data["defaultVersion"] = { version: mainlineRecord.version, summary: mainlineRecord.description, description: mainlineRecord.longDescription, git: mainlineRecord.git, exports: pkgExports, implies: implies }; } else { data["defaultVersion"] = _.last(data.versions); } } return data; }, // Takes in a version record from the official catalog and looks up extra // information that's relevant to this PackageQuery. // // - name: package Name // - version: package version // - summary: version summary/short description (from Package.describe) // - description: long-form description (from the README.md) // - publishedBy: username of the publisher // - publishedOn: date of publication // - git: git URL for this version // - installed: true if the package exists in warehouse, and is therefore // available for use offline. // - architectures: (optional) if self.showArchitecturesOS is true, returns an // array of system architectures for which that package is available. // - dependencies: (optional) if self.showDependencies is true, return an // array of objects denoting that package's dependencies. The objects have // the following keys: // - packageName: name of the dependency // - constraint: constraint for that dependency // - weak: true if this is a weak dependency. _getOfficialVersion: function (versionRecord) { var self = this; var version = versionRecord.version; var name = self.name; var data = { name: name, version: version, summary: versionRecord.description, description: versionRecord.longDescription, publishedBy: versionRecord.publishedBy && versionRecord.publishedBy.username, publishedOn: new Date(versionRecord.published), git: versionRecord.git, exports: versionRecord.exports }; // Get the export and imply data, if the record has any. data["exports"] = new PkgExports(versionRecord.exports); data["implies"] = new PkgImplies(versionRecord.dependencies); // Processing and formatting architectures takes time, so we don't want to // do this if we don't have to. if (self.showArchitecturesOS) { var allBuilds = catalog.official.getAllBuilds(self.name, version); var architectures = _.map(allBuilds, function (build) { if (! build['buildArchitectures']) return "unknown"; var archOS = _.filter(build.buildArchitectures.split('+'), function (arch) { return ( arch !== "web.browser" ) && ( arch !== "web.cordova" ); }); // At this point, you can only have OS arch at a time per-build. return archOS[0]; }); data["architecturesOS"] = architectures; } // Processing and formatting dependencies also takes time, so we would // rather not do it if we don't have to. if (self.showDependencies) { data["dependencies"] = new PkgDependencies(versionRecord.dependencies); } // We want to figure out if we have already downloaded this package, and, // therefore, can use it offline. var tropohouse = self.projectContext.tropohouse; try { data["installed"] = tropohouse.installed({ packageName: name, version: version }); } catch (e) { // Sometimes, we might be unable to determine if the package is installed // -- maybe we don't have access to the directory, or there is some sort // of disk corruption. This might only extend to one version, so it would // be awkward to fail 'meteor show' altogether. Print an error message (if // it is a permissions error, for example, that's something the user might // want to know), but don't throw. Console.printError(e); data["installed"] = false; } return data; }, // Takes in a version record from the local catalog and looks up extra // information that's relevant to this PackageQuery. Returns an object with // the following keys. // // - name: package Name // - version: package version // - summary: version summary/short description (from Package.describe) // - description: long-form description (from the README.md) // - git: git URL for this version // - local: always true (denotes that this is a local package). // - directory: source directory of this package. // - dependencies: (optional) if self.showDependencies is true, return an // array of objects denoting that package's dependencies. The objects have // the following keys: // - packageName: name of the dependency // - constraint: constraint for that dependency // - weak: true if this is a weak dependency. _getLocalVersion: function (localRecord) { var self = this; var data = { name: self.name, summary: localRecord.description, git: localRecord.git, local: true }; // Get the source directory. var packageSource = self.localCatalog.getPackageSource(self.name); data["directory"] = packageSource.sourceRoot; // Get the exports. data["exports"] = new PkgExports(packageSource.getExports()); data["implies"] = new PkgImplies(localRecord.dependencies); // If the version was not explicitly set by the user, the catalog backfills // a placeholder version for the constraint solver. We don't want to show // that version to the user. data["version"] = packageSource.versionExplicitlyProvided ? localRecord.version : "local"; // Processing dependencies takes time, and we don't want to do it if we // don't have to. if (self.showDependencies) { data["dependencies"] = new PkgDependencies(localRecord.dependencies); } var readmeInfo; main.captureAndExit( "=> Errors while reading local packages:", "reading " + data["directory"], function () { readmeInfo = packageSource.processReadme(); }); if (readmeInfo) { data["description"] = readmeInfo.excerpt; } return data; }, // Displays version information from this PackageQuery to the terminal in a // human-friendly format. Takes in an object that contains some, but not all, // of the following keys: // // - name: (mandatory) package Name // - version: (mandatory) package version // - summary: version summary/short description (from Package.describe) // - publishedBy: username of the publisher // - publishedOn: date of publication // - description: long-form description (from the README.md) // - git: git URL for this version. // - local: true for a local version of a package. // - directory: source directory of this package. // - installed: true if the package exists in warehouse, and is therefore // available for use offline. // - architectures: if self.showArchitecturesOS is true, returns an // array of system architectures for which that package is available. // - exports: a PkgExports object, representing package exports. // - exports: a PkgImplies object, representing package implies. // - dependencies: a PkgDependencies object, representing dependencies. _displayVersion: function (data) { var self = this; Console.info( data.name + "@" + data.version, Console.options({ bulletPoint: "Package: " })); if (data.directory) { Console.info("Directory: " + Console.path(data.directory)); } if (data.exports && ! data.exports.isEmpty()) { Console.info( data["exports"].getConsoleStr(), Console.options({ bulletPoint: "Exports: " })); } if (data.implies && ! data.implies.isEmpty()) { Console.info( data["implies"].getConsoleStr(), Console.options({ bulletPoint: "Implies: " })); } if (data.git) { Console.info( Console.url(data.git), Console.options({ bulletPoint: "Git: " })); } // If we don't have a long-form description, print the summary. (If we don't // have a summary, print nothing). if (data.description || data.summary) { Console.info(); Console.info(data.description || data.summary); } // Print dependency information, if the package has any dependencies. if (data.dependencies && ! data.dependencies.isEmpty()) { Console.info(); Console.info("Depends on:"); Console.info( data.dependencies.getConsoleStr(), Console.options({ indent: 2 })); } // Print the 'published by' line at the very bottom. if (data.publishedBy) { var publisher = data.publishedBy; var pubDate = utils.longformDate(data.publishedOn); Console.info(); Console.info("Published by", publisher, "on", pubDate + "."); } // Sometimes, there is a server package and a local package with the same // version. In this case, we prefer the local package. Explain our choice to // the user. if (data.local && catalog.official.getVersion(data.name, data.version)) { Console.info(); Console.info( "This package version is built locally from source.", "The same version of this package also exists on the package server.", "To view its metadata, run", Console.command("'meteor show " + data.name + "@" + data.version + "'"), "from outside the project."); } }, // Returns a user-friendly object from this PackageQuery to the caller. Takes // in a data object with the same keys as _displayVersion. // // Returns an object with some of the following keys: // - name: String. Name of the package. // - version: String. Meteor version number. // - description: String. Longform description. // - summary: String. Short summary. // - git: String. Git URL. // - publishedBy: String. Username of the publisher. // - publishedOn: Date. Time of publication. // - local: Boolean. True if this is a local package. // - directory: source directory of this package. // - installed: Boolean. True if the isopack for this package has been // downloaded, or if the package is local. // - dependencies: Array of objects representing package dependencies, sorted // alphabetically by package name. // - OSarchitectures: Array of OS architectures on for which an isopack of // this package exists (server packages only). // - exports: Array of objects representing the package exports, sorted by // name of export. _generateVersionObject: function (data) { var versionFields = [ "name", "version", "description", "summary", "git", "directory", "publishedBy", "publishedOn", "installed", "local", "architecturesOS", ]; var processedData = {}; _.each(["exports", "implies", "dependencies"], function (key) { processedData[key] = data[key] ? data[key].getObject() : []; }); return _.extend(processedData, _.pick(data, versionFields)); }, // Displays general package data from this PackageQuery to the terminal in a // human-friendly format. Takes in an object that contains some, but not // always all, of the following keys: // // - name: (mandatory) package name // - maintainers: array of usernames of maintainers // - homepage: string of the package homepage // - defaultVersion: the default version of this package to use for looking up // per-version information that's relevant to the package in general (ex: // git). // - totalVersions: the total number of versions that this package has, // including hidden versions. // - versions: an ordered array of objects, representing the versions of this // package that we should return to the user. Each version should contain // some of the following keys: // - version: (mandatory) version number, or "local" for a version-less // local package. // - publishedOn: the date that the package was published. // - installed: true if this is a server package that has already been // downloaded to the warehouse. // - local: true for a local package. // - directory: source root directory of a local package. // - hiddenVersions: an object containing some information about versions that // have been hidden from the user. Has keys: // - oldestVersion: the version of this package with the smallest Meteor // semver number that exists in our records. // - lastUnmigrated: the most recent (largest Meteor semver) version that // is marked 'unmigrated'. // - lastPreRelease: the most recent pre-release version. _displayPackage: function (data) { var self = this; var defaultVersion = data.defaultVersion; // Every package has a name. Some packages have a homepage. var displayName = data.defaultVersion ? data.name + "@" + data.defaultVersion.version : data.name; Console.info(displayName, Console.options({ bulletPoint: "Package: " })); if (data.homepage) { Console.info(Console.url(data.homepage), Console.options({ bulletPoint: "Homepage: " })); } // Local packages might not have any maintainers. if (! _.isEmpty(data.maintainers)) { Console.info(data.maintainers.join(", "), Console.options({ bulletPoint: "Maintainers: " })); } // Git is per-version, so we will print the latest one, if one exists. if (defaultVersion && defaultVersion.git) { Console.info(Console.url(defaultVersion.git), Console.options({ bulletPoint: "Git: " })); } // Print the exports. if (defaultVersion && defaultVersion.exports && ! defaultVersion.exports.isEmpty()) { Console.info( defaultVersion["exports"].getConsoleStr(), Console.options({ bulletPoint: "Exports: " })); } if (defaultVersion && defaultVersion.implies && ! defaultVersion.implies.isEmpty()) { Console.info( defaultVersion["implies"].getConsoleStr(), Console.options({ bulletPoint: "Implies: " })); } Console.info(); // If we don't have a long-form description, we will use the summary. For a // local package, we might not have a summary, in which case we should be // careful not to print extra lines. var printDescription = defaultVersion && (defaultVersion.description || defaultVersion.summary); if (printDescription) { Console.info(printDescription ); Console.info(); } // If we have any versions to show, print them out now. var versionRows = []; if (data.versions && ! _.isEmpty(data.versions)) { var versionsHeader = self.showHiddenVersions ? "Versions:" : "Recent versions:"; Console.info(versionsHeader); _.each(data.versions, function (v) { // For a local package, we don't have a published date, and we don't // need to show if it has already been downloaded (it is local, we don't // need to download it). Instead of showing both of these values, let's // show the directory. if (v.local) { versionRows.push([v.version, v.directory]); return; } // Convert the date into a display-friendly format, or print nothing for // a local package. var publishDate = utils.longformDate(v.publishedOn); // If there is a status that we would like to report for this package, // figure it out now. if (v.installed) { var paddedDate = padLongformDate(publishDate); versionRows.push([v.version, paddedDate + " " + "installed"]); } else { versionRows.push([v.version, publishDate]); } }); // The only time that we are going to go over a reasonable character limit // is with a directory for the local package. We would much rather display // the full directory than trail it off. Console.printTwoColumns(versionRows, { indent: 2, ignoreWidth: true }); } // If we have not shown all the available versions, let the user know. if (data.totalVersions > versionRows.length) { var oldestShownVersion = (data["versions"][0] && data["versions"][0].version) || ""; // A string explaining why those versions have been hidden. var hiddenVersions = formatHiddenVersions(data["hiddenVersions"], oldestShownVersion); // We will word things in the message in different ways, based on whether // multiple versions exist/have been hidden. var hiddenVersionsPluralizer = (data.totalVersions - data.versions.length == 1) ? "One " + hiddenVersions + " version of " + self.name + " has" : hiddenVersions[0].toUpperCase() + hiddenVersions.slice(1) + " versions of " + self.name + " have"; var allVersionsPluralizer = (data.totalVersions === 1) ? "the hidden version" : "all " + data.totalVersions + " versions"; // Display the final message. Console.info( hiddenVersionsPluralizer, "been hidden.", "To see " + allVersionsPluralizer + ", run", Console.command("'meteor show --show-all " + self.name + "'") + "."); } }, // Returns a user-friendly object from this PackageQuery to the caller. Takes // in a data object with the same keys as _displayPackage. // // Returns an object with some of the following keys: // - name: String. Name of the package. // - homepage: String. URL of the package homepage. // - maintainers: Array of strings. Usernames of package maintainers. // - totalVersions: Number. Total number of versions that exist for this // package. // - versions: Array of objects, representing versions of this // package. Objects have the following keys: // - name: String. Name of the package. // - version: String. Meteor version number. // - description: String. Longform description. // - summary: String. Short summary. // - git: String. Git URL. // - publishedBy: String. Username of the publisher. // - publishedOn: Date. Time of publication. // - local: Boolean. True if this is a local package. // - directory: source directory of this package. // - installed: Boolean. True if the isopack for this package has been // downloaded, or if the package is local. // - exports: Array of objects representing the package exports, sorted by // name of export. _generatePackageObject: function (data) { var packageFields = [ "name", "homepage", "maintainers", "totalVersions" ]; // Process the versions array. We only want some of the keys, and we want to // make sure to get the right exports object. var versions = _.map(data["versions"], function (version) { var versionFields = [ "name", "version", "description", "summary", "git", "publishedBy", "publishedOn", "installed", "local", "directory", "architecturesOS" ]; var processedData = {}; _.each(["exports", "implies"], function (key) { processedData[key] = version[key] ? version[key].getObject() : []; }); return _.extend(processedData, _.pick(version, versionFields)); }); return _.extend({ versions: versions }, _.pick(data, packageFields)); }, }); // This class looks up release-related information in the official catalog. // // The constructor takes in an object with the following keys: // - metaRecord: (mandatory) the meta-record for this release from the // Releases collection. // - version: specific version of a release that we want to query. // - showHiddenVersions: show experimental, pre-release & otherwise // non-recommended versions of this release. var ReleaseQuery = function (options) { var self = this; // This is the record in the Releases collection. Contains metadata, such as // maintainers. self.metaRecord = options.metaRecord; self.name = options.metaRecord.name; // We don't always want to show non-recommended release versions. self.showHiddenVersions = options.showHiddenVersions; // Aggregate the query data. If we are asking for a specific version, get data // for a specific version, otherwise aggregate the data about this release // track in general. self.data = options.version ? self._getVersionDetails(options.version) : self._getReleaseData(); }; _.extend(ReleaseQuery.prototype, { // Prints the data from this ReleaseQuery to the terminal. Takes the following // options: // - ejson: Don't pretty-print the data. Return a machine-readable ejson // object. print: function (options) { var self = this; // If we are asking for an EJSON-style output, print out the relevant fields. if (options.ejson) { var versionFields = [ "track", "version", "description", "publishedBy", "publishedOn", "tool", "packages", "recommended" ]; var packageFields = [ "name", "maintainers", "versions" ]; var fields = self.data.version ? versionFields : packageFields; Console.rawInfo(formatEJSON(_.pick(self.data, fields))); return; } // If we are asking for a specific version, display the information about // that version. if (self.data.version) { self._displayVersion(self.data); return; } // Otherwise, print the data about this release track in general. self._displayRelease(self.data); }, // Gets detailed data about a specific version of this release. Returns an // object with the following keys: // - track: name of the release track // - version: release version // - description: description of the release version // - recommended: if this is a recommended version. // - orderKey: the orderKey of this version // - publishedBy: username of the publisher // - publishedOn: date this version was published // - packages: map of packages that go into this version // - tool: the tool package@version for this release version _getVersionDetails: function (version) { var self = this; var versionRecord = catalog.official.getReleaseVersion(self.name, version); if (! versionRecord) { return null; } var publishDate = getReleaseVersionPublishedOn(versionRecord); return { track: self.name, version: version, description: versionRecord.description, recommended: versionRecord.recommended, orderKey: versionRecord.orderKey, publishedBy: versionRecord.publishedBy["username"], pubishedOn: publishDate, packages: versionRecord.packages, tool: versionRecord.tool }; }, // Gets aggregate data about this release track in general. Returns an object // with the following keys: // - track: name of the release track // - maintainers: an array of usernames of maintainers // - defaultVersion: version record for the default version of this release. // - totalVersions: total number of release versions for this track // - versions: an array of version objects. If only recommended versions // are returned, ordered by orderKey, otherwise unordered. Objects have // the following keys: // - version: version number // - description: version description // - recommended: true for recommended versions // - orderKey: (only if showHiddenVersions is true) the orderKey of // this version. // - publishedBy: username of the publisher // - publishedOn: date the version was published _getReleaseData: function () { var self = this; var data = { track: self.metaRecord.name, maintainers: _.pluck(self.metaRecord.maintainers, "username") }; data["defaultVersion"] = catalog.official.getDefaultReleaseVersionRecord(self.name); // Collect information about versions. var versions; if (self.showHiddenVersions) { // There is no obvious way to get an absolute ranking of all release // versions, so this is unsorted. If we have to, we will deal with sorting // this at display time. versions = catalog.official.getReleaseVersionRecords(self.name); } else { versions = catalog.official.getSortedRecommendedReleaseRecords(self.name); versions.reverse(); } // We don't want to show the user package or tool data in general release // mode (it is a lot of data). Select to show the fields that we want to // return only. var versionFields = [ "version", "description", "recommended"]; // orderKey is important for dealing with experimental versions, but it is // an internal system detail that we would rather not reveal at this level. if (self.showHiddenVersions) { versionFields.push("orderKey"); } data["versions"] = _.map(versions, function (versionRecord) { var data = _.pick(versionRecord, versionFields); data.publishedBy = versionRecord.publishedBy["username"]; data.publishedOn = getReleaseVersionPublishedOn(versionRecord); return data; }); data["totalVersions"] = catalog.official.getNumReleaseVersions(self.name); return data; }, // Displays information about a specific release version in a human-readable // format. Takes in an object with the following keys: // - track: release track // - version: release version // - publishedBy: username of the publisher // - publishedOn: date the version was published // - recommended: true if this is a recommended version // - description: description of the release version // - tool: tool package specification for this version // - packages: map of packages for this release version _displayVersion: function (data) { var self = this; Console.info("Release: " + data.track + "@" + data.version); var isRecommended = data.recommended ? "yes" : "no"; Console.info("Recommended: " + isRecommended); Console.info("Tool package: " + data.tool); Console.info(); Console.info(data.description); Console.info(); if (!_.isEmpty(data.packages)) { Console.info("Packages:"); _.each(data.packages, function (version, package) { Console.info( package + ": " + version, Console.options({ indent: 2 })); }); Console.info(); } Console.info( "Published by " + data.publishedBy + " on " + utils.longformDate(getReleaseVersionPublishedOn(data))); }, // Displays information about this release track in general in a // human-readable format. Takes in an object with the following keys: // - track: name of the release track // - maintainers: an array of usernames of maintainers // - defaultVersion: version record for the default version of this release. // - totalVersions: total number of release versions for this track // - versions: an array of version objects. If only recommended versions // are returned, ordered by orderKey, otherwise unordered. Objects have // the following keys: // - version: version number // - description: version description // - recommended: true for recommended versions // - orderKey: (only if showHiddenVersions is true) the orderKey of // this version. // - publishedBy: username of the publisher // - publishedOn: date the version was published _displayRelease: function (data) { var self = this; Console.info("Release:", data.track); // There is no such thing as a local release, which means all releases have // a maintainer. Console.info("Maintainers:", data.maintainers.join(", ")); Console.info(); if (data.defaultVersion) { Console.info(data.defaultVersion.description); Console.info(); } if (self.showHiddenVersions) { self._displayAllReleaseVersions(data.versions); return; } // Display the recommended versions of this release. var rows = []; if (!_.isEmpty(data.versions)) { Console.info("Recommended versions:"); _.each(data.versions, function (v) { rows.push([v.version, utils.longformDate(v.publishedOn)]); }); Console.printTwoColumns(rows, { indent: 2 }); }; // Display a warning about other release versions at the bottom. if (data.totalVersions > rows.length) { var versionsPluralizer = (data.totalVersions > 1) ? "all " + data.totalVersions + " versions" : "the hidden version"; // We only hide release versions for one reason -- they are not // recommended. We would have to parse version numbers to differentiate // between 'pre-release' and 'deprecated' (and sort-of-experimental, like // '1.0-weird-trick) and we don't want to rely on version number // conventions in code. var versionsHidden = (data.totalVersions - rows.length > 1) ? "Non-recommended versions of " + self.name + " have been hidden." : "One non-recommended version of " + self.name + " has been hidden."; Console.info( versionsHidden, "To see " + versionsPluralizer + ", run", Console.command("'meteor show --show-all " + self.name + "'") + "."); } }, // Displays all the versions of a given release in a human-readable // format. Includes experimental and otherwise hidden versions. Takes in an // array of version objects, each of which has the following keys: // - version: version string // - orderKey: (optional) orderKey of this version. Not all versions have // orderKeys. // - publishedOn: date of publication // - recommended: true if the version is recommended. _displayAllReleaseVersions: function (versions) { var self = this; var columnOpts = { indent: 2, ignoreWidth: true }; // If we don't have any versions, then there is nothing to display. if (! versions) { return; } // We are going to print versions with order key ('versions'), separately // from versions without an order key ('experimental versions'). var versionsDivided = _.groupBy(versions, function (v) { return _.has(v, "orderKey"); }); var experimentalVersions = versionsDivided[false]; var versionsWithKey = versionsDivided[true]; if (versionsWithKey) { // Sort versions that have order keys by order key, so that 1.0 comes // after 0.9.4.1, etc. versionsWithKey = _.sortBy(versionsWithKey, function (v) { return v.orderKey; }); Console.info("Versions:"); var rows = []; _.each(versionsWithKey, function (vr) { var dateStr = utils.longformDate(vr.publishedOn); if (! vr.recommended) { rows.push([ vr.version, dateStr ]); } else { var paddedDate = padLongformDate(dateStr); rows.push([ vr.version, paddedDate + " (recommended)" ]); } }); Console.printTwoColumns(rows, columnOpts); } if (experimentalVersions) { // We can't sort by order key, so sort by order of publication. experimentalVersions = _.sortBy(experimentalVersions, function (v) { return v.publishedOn; }); Console.info("Experimental versions:"); var rows = []; _.each(experimentalVersions, function (vr) { // Experimental versions cannot be recommended. rows.push([vr.version, utils.longformDate(vr.publishedOn)]); }); Console.printTwoColumns(rows, columnOpts); } } }); /////////////////////////////////////////////////////////////////////////////// // show /////////////////////////////////////////////////////////////////////////////// main.registerCommand({ name: 'show', pretty: true, minArgs: 0, maxArgs: 1, usesPackage: true, options: { "show-all": { type: Boolean }, "ejson": { type: Boolean } }, catalogRefresh: new catalog.Refresh.OnceAtStart( { maxAge: DEFAULT_MAX_AGE_MS, ignoreErrors: true }) }, function (options) { var fullName; var name; var version; // Because of the new projectContext interface, we need to initialize the // project context in order to load the local catalog. This is not ideal. var projectContext = getTempContext(options); // If the user specified a query, process it. if (! _.isEmpty(options.args)) { // The foo@bar API means that we have to do some string parsing to figure out // if we want a particular version. fullName = options.args[0]; var splitArgs = fullName.split('@'); name = splitArgs[0]; version = (splitArgs.length > 1) ? splitArgs[1] : null; if (splitArgs.length > 2) { Console.error("Invalid request format: " + fullName); process.exit(1); } } else { if (! options.packageDir) { // Letting the user run 'meteor show' without arguments from a package // directory is a pleasant shortcut, but the default should be specifying // a query. Console.error( "Please specify a package or release name to show information about it." ); process.exit(1); } // Use the projectContext to get the name of the package. var currentVersion = projectContext.localCatalog.getVersionBySourceRoot(options.packageDir); name = currentVersion.packageName; version = "local"; fullName = name + "@local"; } var query = null; // First, we need to figure out if we are dealing with a package, or a // release. We don't want to rely on capitalization conventions, so we will // start by checking if a package by that name exists. If it does, then we are // dealing with a package. (Unlike the normal projectContext, we want to // prefer the remote record, if one exists, rather than the local record. The // remote record contains data like 'homepage' and 'maintainers', that the // local record does not). var packageRecord = catalog.official.getPackage(name) || projectContext.localCatalog.getPackage(name); if (packageRecord) { query = new PackageQuery({ metaRecord: packageRecord, version: version, projectContext: projectContext, showHiddenVersions: options["show-all"], showArchitecturesOS: options.ejson, showDependencies: !! version }); } // If this is not a package, it might be a release. Let's check if there is // a release by this name. There are no local releases, so we only need to // check the official catalog. if (! query) { var releaseRecord = catalog.official.getReleaseTrack(name); if (releaseRecord) { query = new ReleaseQuery({ metaRecord: releaseRecord, version: version, showHiddenVersions: options["show-all"] }); } } // If we have failed to create a query, or if we have created a query and it // couldn't gather any data about our request, then the item that we are // looking for does not exist. if (! query || ! query.data) { return itemNotFound(fullName); } query.print({ ejson: !! options.ejson }); return 0; }); /////////////////////////////////////////////////////////////////////////////// // search /////////////////////////////////////////////////////////////////////////////// main.registerCommand({ name: 'search', pretty: true, usesPackage: true, minArgs: 0, // So we can provide specific help maxArgs: 1, options: { maintainer: { type: String }, "show-all": { type: Boolean }, ejson: { type: Boolean }, // Undocumented debug-only option for Velocity. "debug-only": { type: Boolean } }, catalogRefresh: new catalog.Refresh.OnceAtStart( { maxAge: DEFAULT_MAX_AGE_MS, ignoreErrors: true }) }, function (options) { if (options.args.length === 0) { Console.info( "To show all packages, do", Console.command("meteor search .")); return 1; } // Because of the new projectContext interface, we need to initialize the // project context in order to load the local catalog. var projectContext = getTempContext(options); // XXX We should push the queries into SQLite! var allPackages = _.union( catalog.official.getAllPackageNames(), projectContext.localCatalog.getAllPackageNames()); var allReleases = catalog.official.getAllReleaseTracks(); var matchingPackages = []; var matchingReleases = []; var selector; var pattern = options.args[0]; var search; try { search = new RegExp(pattern); } catch (err) { Console.error(err + ""); return 1; } // Do not return true on broken packages, unless requested in options. var filterBroken = function (match, isRelease, name) { // If the package does not match, or it is not a package at all or if we // don't want to filter anyway, we do not care. if (!match || isRelease) return match; var vr; if (!options["show-all"]) { // If we can't find a version in the local catalog, we want to get the // latest mainline (ie: non-RC) version from the official catalog. vr = projectContext.localCatalog.getLatestVersion(name) || catalog.official.getLatestMainlineVersion(name); } else { // We want the latest version of this package, and we don't care if it is // a release candidate. vr = projectContext.projectCatalog.getLatestVersion(name); } if (!vr) { return false; } // If we did NOT ask for unmigrated packages and this package is unmigrated, // we don't care. if (!options["show-all"] && vr.unmigrated){ return false; } // If we asked for debug-only packages and this package is NOT debug only, // we don't care. if (options["debug-only"] && !vr.debugOnly) { return false; } return true; }; if (options.maintainer) { var username = options.maintainer; // In the future, we should consider checking this on the server, but I // suspect the main use of this command will be to deal with the automatic // migration and uncommon in everyday use. From that perspective, it makes // little sense to require you to be online to find out what packages you // own; and the consequence of not mentioning your group packages until // you update to a new version of meteor is not that dire. selector = function (name, isRelease) { var record; // XXX make sure search works while offline if (isRelease) { record = catalog.official.getReleaseTrack(name); } else { record = catalog.official.getPackage(name); } return filterBroken( (name.match(search) && record && !!_.findWhere(record.maintainers, {username: username})), isRelease, name); }; } else { selector = function (name, isRelease) { return filterBroken(name.match(search), isRelease, name); }; } buildmessage.enterJob({ title: 'Searching packages' }, function () { _.each(allPackages, function (pack) { if (selector(pack, false)) { var vr; if (!options['show-all']) { vr = projectContext.localCatalog.getLatestVersion(pack) || catalog.official.getLatestMainlineVersion(pack); } else { vr = projectContext.projectCatalog.getLatestVersion(pack); } if (vr) { matchingPackages.push({ name: pack, description: vr.description, latestVersion: vr.version, lastUpdated: new Date(vr.lastUpdated) }); } } }); _.each(allReleases, function (track) { if (selector(track, true)) { var vr = catalog.official.getDefaultReleaseVersionRecord(track); if (vr) { matchingReleases.push({ name: track, description: vr.description, latestVersion: vr.version, lastUpdated: new Date(vr.lastUpdated) }); } } }); }); if (options.ejson) { var ret = { packages: matchingPackages, releases: matchingReleases }; Console.rawInfo(formatEJSON(ret)); return 0; } var output = false; if (!_.isEqual(matchingPackages, [])) { output = true; Console.info("Matching packages:"); utils.printPackageList(matchingPackages); } if (!_.isEqual(matchingReleases, [])) { output = true; Console.info("Matching releases:"); utils.printPackageList(matchingReleases); } if (!output) { Console.error(pattern + ': nothing found'); utils.explainIfRefreshFailed(); } else { Console.info( "You can use", Console.command("'meteor show'"), "to get more information on a specific item."); } });