From 4300d261f3a265a8e29f06fdc9974ed2ee330cdf Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Wed, 12 Oct 2016 11:41:35 -0400 Subject: [PATCH] Support `meteor ` for any dev_bundle/bin/ executable. This will make it easier to use tools like https://yarnpkg.com/ with the right version of Node, etc. With this commit, here's all you have to do: meteor npm install -g yarnpkg Then test that it works: meteor yarn info Note that any commands registered by Meteor itself will not be honored. --- History.md | 3 ++ tools/cli/dev-bundle-bin-commands.js | 17 ++++----- tools/cli/dev-bundle-bin-helpers.js | 53 ++++++++++++++++++++++++++++ tools/cli/main.js | 22 ++++++++++++ 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/History.md b/History.md index 0bd8ebb202..30054ae735 100644 --- a/History.md +++ b/History.md @@ -48,6 +48,9 @@ sensitive to non-reproducible factors in the external environment. https://github.com/meteor/meteor/pull/7668/commits/3313180a6ff33ee63602f7592a9506012029e919 +* The `meteor ...` syntax will now work for any command + installed in `dev_bundle/bin`, except for Meteor's own commands. + * The `meteor test` command now supports the `--no-release-check` flag. https://github.com/meteor/meteor/pull/7668/commits/7097f78926f331fb9e70a06300ce1711adae2850 diff --git a/tools/cli/dev-bundle-bin-commands.js b/tools/cli/dev-bundle-bin-commands.js index 5988abe27d..c9325b9799 100644 --- a/tools/cli/dev-bundle-bin-commands.js +++ b/tools/cli/dev-bundle-bin-commands.js @@ -1,11 +1,8 @@ // Note that this file is required before we install our Babel hooks in // ../tool-env/install-babel.js, so we can't use ES2015+ syntax here. +var fs = require("fs"); var path = require("path"); -var win32Extensions = { - node: ".exe", - npm: ".cmd" -}; // The dev_bundle/bin command has to come immediately after the meteor // command, as in `meteor npm` or `meteor node`, because we don't want to @@ -14,22 +11,22 @@ var devBundleBinCommand = process.argv[2]; var args = process.argv.slice(3); function getChildProcess() { - if (! win32Extensions.hasOwnProperty(devBundleBinCommand)) { + if (typeof devBundleBinCommand !== "string") { return Promise.resolve(null); } var helpers = require("./dev-bundle-bin-helpers.js"); - if (process.platform === "win32") { - devBundleBinCommand += win32Extensions[devBundleBinCommand]; - } - return Promise.all([ helpers.getDevBundle(), helpers.getEnv() ]).then(function (devBundleAndEnv) { var devBundleDir = devBundleAndEnv[0]; - var cmd = path.join(devBundleDir, "bin", devBundleBinCommand); + var cmd = helpers.getCommand(devBundleBinCommand, devBundleDir); + if (! cmd) { + return null; + } + var env = devBundleAndEnv[1]; var child = require("child_process").spawn(cmd, args, { stdio: "inherit", diff --git a/tools/cli/dev-bundle-bin-helpers.js b/tools/cli/dev-bundle-bin-helpers.js index 23aa66955a..665b34702a 100644 --- a/tools/cli/dev-bundle-bin-helpers.js +++ b/tools/cli/dev-bundle-bin-helpers.js @@ -1,12 +1,65 @@ var fs = require("fs"); var path = require("path"); var files = require("../fs/mini-files.js"); +var isWindows = process.platform === "win32"; +var extensions = isWindows ? [".cmd", ".exe"] : [""]; +var hasOwn = Object.prototype.hasOwnProperty; function getDevBundle() { return require("./dev-bundle.js"); } exports.getDevBundle = getDevBundle; +exports.getCommand = function (name, devBundleDir) { + var result = null; + + // Strip leading and/or trailing whitespace. + name = name.replace(/^\s+|\s+$/g, ""); + + if (! isValidCommand(name, devBundleDir)) { + return result; + } + + extensions.some(function (ext) { + var cmd = path.join(devBundleDir, "bin", name + ext); + try { + if (fs.statSync(cmd).isFile()) { + result = cmd; + return true; + } + } catch (e) { + return false; + } + }); + + return result; +}; + +function isValidCommand(name, devBundleDir) { + if (name === "node" || + name === "npm") { + return true; + } + + if (! name || name.charAt(0) === ".") { + // Disallow empty commands and commands that start with a period. + return false; + } + + var meteorCommandsJsonPath = + path.join(devBundleDir, "bin", ".meteor-commands.json"); + + try { + var meteorCommands = require(meteorCommandsJsonPath); + } catch (e) { + return false; + } + + // If `meteor ` is already a Meteor command, don't let anything in + // dev_bundle/bin override it. + return ! hasOwn.call(meteorCommands, name); +} + exports.getEnv = function (options) { var devBundle = options && options.devBundle; var devBundlePromise = typeof devBundle === "string" diff --git a/tools/cli/main.js b/tools/cli/main.js index 5d8b1c3942..611d4a7f78 100644 --- a/tools/cli/main.js +++ b/tools/cli/main.js @@ -291,6 +291,28 @@ require('./commands-packages-query.js'); require('./commands-cordova.js'); require('./commands-aliases.js'); +/////////////////////////////////////////////////////////////////////////////// +// Record all the top-level commands as JSON +/////////////////////////////////////////////////////////////////////////////// + +export const meteorCommandsJsonPath = files.pathJoin( + files.getDevBundle(), "bin", ".meteor-commands.json" +); + +export function dumpMeteorCommands() { + const all = Object.create(null); + Object.keys(commands).forEach(name => all[name] = true); + const json = JSON.stringify(all, null, 2); + files.writeFile(meteorCommandsJsonPath, json + "\n"); + return all; +} + +if (files.inCheckout()) { + // If we're running Meteor from a checkout, dump the commands every + // time, so that the file remains up to date. + dumpMeteorCommands(); +} + /////////////////////////////////////////////////////////////////////////////// // Long-form help ///////////////////////////////////////////////////////////////////////////////