From 833ca598bc7553cd60080705da076454ab5c544b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Bigio?= Date: Tue, 22 Sep 2015 09:00:04 -0700 Subject: [PATCH] Introduce react native CLI Reviewed By: @frantic Differential Revision: D2430522 --- packager/parseCommandLine.js | 5 +- private-cli/index.js | 25 ++++++ private-cli/src/cli.js | 62 +++++++++++++ private-cli/src/dependencies/dependencies.js | 93 ++++++++++++++++++++ private-cli/src/util/Config.js | 60 +++++++++++++ private-cli/src/util/__mocks__/log.js | 12 +++ private-cli/src/util/log.js | 19 ++++ 7 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 private-cli/index.js create mode 100644 private-cli/src/cli.js create mode 100644 private-cli/src/dependencies/dependencies.js create mode 100644 private-cli/src/util/Config.js create mode 100644 private-cli/src/util/__mocks__/log.js create mode 100644 private-cli/src/util/log.js diff --git a/packager/parseCommandLine.js b/packager/parseCommandLine.js index 36b8ab119..4293b79b9 100644 --- a/packager/parseCommandLine.js +++ b/packager/parseCommandLine.js @@ -20,7 +20,8 @@ var optimist = require('optimist'); -function parseCommandLine(config) { +function parseCommandLine(config, args) { + args = args || process.argv; // optimist default API requires you to write the command name three time // This is a small wrapper to accept an object instead for (var i = 0; i < config.length; ++i) { @@ -38,7 +39,7 @@ function parseCommandLine(config) { optimist.demand(config[i].command); } } - var argv = optimist.parse(process.argv); + var argv = optimist.parse(args); // optimist doesn't have support for --dev=false, instead it returns 'false' for (var i = 0; i < config.length; ++i) { diff --git a/private-cli/index.js b/private-cli/index.js new file mode 100644 index 000000000..51fba7207 --- /dev/null +++ b/private-cli/index.js @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +require('babel-core/register')({ + only: [ + /react-native-github\/private-cli\/src/ + ], +}); + +var cli = require('./src/cli'); +var fs = require('fs'); +var gracefulFs = require('graceful-fs'); + +// graceful-fs helps on getting an error when we run out of file +// descriptors. When that happens it will enqueue the operation and retry it. +gracefulFs.gracefulify(fs); + +module.exports = cli; diff --git a/private-cli/src/cli.js b/private-cli/src/cli.js new file mode 100644 index 000000000..c81fa9a32 --- /dev/null +++ b/private-cli/src/cli.js @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const Config = require('./util/Config'); +const dependencies = require('./dependencies/dependencies'); +const Promise = require('promise'); + +const documentedCommands = { + dependencies: dependencies, +}; + +const hiddenCommands = { + '-h': help, + '--help': help, +}; + +/** + * Programmatic entry point for the cli. This function runs the given + * command passing it the arguments array. + */ +function run(command, commandArgs) { + if (!command) { + throw new Error(helpMessage()); + } + commandArgs = commandArgs || []; + + const commandToExec = documentedCommands[command] || hiddenCommands[command]; + if (!commandToExec) { + throw new Error(helpMessage(command)); + } + + commandToExec(commandArgs, Config.get()).done(); +} + +function helpMessage(command) { + const validCommands = Object + .keys(documentedCommands) + .map(c => '"' + c + '"') + .join(' | '); + + if (command) { + return 'Unknown command "' + command + '". ' + + 'Available commands: ' + validCommands; + } else { + return 'Must specify a command. Available commands: ' + + validCommands; + } +} + +function help() { + console.log(helpMessage()); + return Promise.resolve(); +} + +module.exports.run = run; diff --git a/private-cli/src/dependencies/dependencies.js b/private-cli/src/dependencies/dependencies.js new file mode 100644 index 000000000..e4c9ae785 --- /dev/null +++ b/private-cli/src/dependencies/dependencies.js @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const fs = require('fs'); +const log = require('../util/log').out('dependencies'); +const parseCommandLine = require('../../../packager/parseCommandLine'); +const path = require('path'); +const Promise = require('Promise'); +const ReactPackager = require('../../../packager/react-packager'); + +/** + * Returns the dependencies an entry path has. + */ +function dependencies(argv, conf) { + return new Promise((resolve, reject) => { + _dependencies(argv, conf, resolve, reject); + }); +} + +function _dependencies(argv, conf, resolve, reject) { + const args = parseCommandLine([ + { + command: 'entry-file', + description: 'Absolute path to the root JS file', + type: 'string', + required: true, + }, { + command: 'output', + description: 'File name where to store the output, ex. /tmp/dependencies.txt', + type: 'string', + } + ], argv); + + const rootModuleAbsolutePath = args['entry-file']; + if (!fs.existsSync(rootModuleAbsolutePath)) { + reject(`File ${rootModuleAbsolutePath} does not exist`); + } + + const config = { + projectRoots: conf.getProjectRoots(), + assetRoots: conf.getAssetRoots(), + blacklistRE: conf.getBlacklistRE(), + transformModulePath: conf.getTransformModulePath(), + }; + + const relativePath = config.projectRoots.map(root => + path.relative( + root, + rootModuleAbsolutePath + ) + )[0]; + + const writeToFile = args.output; + const outStream = writeToFile + ? fs.createWriteStream(args.output) + : process.stdout; + + log('Running ReactPackager'); + log('Waiting for the packager.'); + resolve(ReactPackager.createClientFor(config).then(client => { + log('Packager client was created'); + return client.getDependencies(relativePath) + .then(deps => { + log('Packager returned dependencies'); + client.close(); + + deps.forEach(module => { + // Temporary hack to disable listing dependencies not under this directory. + // Long term, we need either + // (a) JS code to not depend on anything outside this directory, or + // (b) Come up with a way to declare this dependency in Buck. + const isInsideProjectRoots = config.projectRoots.filter(root => + module.path.startsWith(root) + ).length > 0; + + if (isInsideProjectRoots) { + outStream.write(module.path + '\n'); + } + }); + writeToFile && outStream.end(); + log('Wrote dependencies to output file'); + }); + })); +} + +module.exports = dependencies; diff --git a/private-cli/src/util/Config.js b/private-cli/src/util/Config.js new file mode 100644 index 000000000..ccd2a43c8 --- /dev/null +++ b/private-cli/src/util/Config.js @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const RN_CLI_CONFIG = 'rn-cli.config.js'; +let cachedConfig = null; + +/** + * Module capable of getting the configuration that should be used for + * the `rn-cli`. The configuration file is a JS file named `rn-cli.conf.js`. + * It has to be on any parent directory of the cli. + */ +const Config = { + get() { + if (cachedConfig) { + return cachedConfig; + } + + const parentDir = findParentDirectory(__dirname, RN_CLI_CONFIG); + + if (!parentDir) { + throw new Error( + 'Can\'t find "rn-cli.config.js" file in any parent folder of "' + + __dirname + '"' + ); + } + + cachedConfig = require(path.join(parentDir, RN_CLI_CONFIG)); + return cachedConfig; + } +}; + +// Finds the most near ancestor starting at `currentFullPath` that has +// a file named `filename` +function findParentDirectory(currentFullPath, filename) { + const root = path.parse(currentFullPath).root; + const testDir = (parts) => { + if (parts.length === 0) { + return null; + } + + const fullPath = path.join(root, parts.join(path.sep)); + + var exists = fs.existsSync(path.join(fullPath, filename)); + return exists ? fullPath : testDir(parts.slice(0, -1)); + }; + + return testDir(currentFullPath.substring(1).split(path.sep)); +} + +module.exports = Config; diff --git a/private-cli/src/util/__mocks__/log.js b/private-cli/src/util/__mocks__/log.js new file mode 100644 index 000000000..5fff767d2 --- /dev/null +++ b/private-cli/src/util/__mocks__/log.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +module.exports.out = () => jest.genMockFn(); +module.exports.err = () => jest.genMockFn(); diff --git a/private-cli/src/util/log.js b/private-cli/src/util/log.js new file mode 100644 index 000000000..018c5275a --- /dev/null +++ b/private-cli/src/util/log.js @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +function log(stream, module) { + return function() { + const message = Array.prototype.slice.call(arguments).join(' '); + stream.write(module + ': ' + message + '\n'); + }; +} + +module.exports.out = log.bind(null, process.stdout); +module.exports.err = log.bind(null, process.stderr);