Merge `rnpm config` with the `cli config`, step 1

Summary:
This is step one on merging the `rnpm` config with the `cli config`. The plan is to remove two sources of truth (rnpm package.json config and rn-cli Javascript config)

Rationale:
As of now, we have `rnpm` config, that used to live in the `local-cli/core/config/index.js` and the `rn-cli` config, living in `default.config.js` and `utils/Config.js`.

This PR moves all the things into one file, called `local-cli/core', simplifies things, enhances types (Config now has better types, added missing properties and fixed descriptions).

One notable addition is that users can now opt-in to override the commands that are loaded at the package level. Previously it was only possible to add a command by writing a plugin. Now, you can just tweak the `rn-cli.config.js`.

This is one of the several improvements I have on my roadmap, with better documentation for the CLI as well.
Closes https://github.com/facebook/react-native/pull/11564

Differential Revision: D4360095

fbshipit-source-id: feaec7c88df63e51cef1f9620c7eedeb738d3d00
This commit is contained in:
Mike Grabowski 2017-01-09 07:49:29 -08:00 committed by Facebook Github Bot
parent 2c5bf97928
commit 30e89b49f1
33 changed files with 234 additions and 237 deletions

View File

@ -21,7 +21,7 @@ const saveAssets = require('./saveAssets');
const defaultAssetExts = require('../../packager/defaults').assetExts;
import type {RequestOptions, OutputOptions} from './types.flow';
import type {ConfigT} from '../util/Config';
import type {ConfigT} from '../core';
function saveBundle(output, bundle, args) {
return Promise.resolve(

View File

@ -10,21 +10,19 @@
*/
'use strict';
const Config = require('./util/Config');
const config = require('./core');
const assertRequiredOptions = require('./util/assertRequiredOptions');
const chalk = require('chalk');
const childProcess = require('child_process');
const commander = require('commander');
const commands = require('./commands');
const defaultConfig = require('./default.config');
const init = require('./init/init');
const minimist = require('minimist');
const path = require('path');
const pkg = require('../package.json');
import type {Command} from './commands';
import type {ConfigT} from './util/Config';
import type {CommandT} from './commands';
import type {ConfigT} from './core';
commander.version(pkg.version);
@ -93,7 +91,7 @@ function printUnknownCommand(cmdName) {
].join('\n'));
}
const addCommand = (command: Command, config: ConfigT) => {
const addCommand = (command: CommandT, cfg: ConfigT) => {
const options = command.options || [];
const cmd = commander
@ -108,7 +106,7 @@ const addCommand = (command: Command, config: ConfigT) => {
Promise.resolve()
.then(() => {
assertRequiredOptions(options, passedOptions);
return command.func(argv, config, passedOptions);
return command.func(argv, cfg, passedOptions);
})
.catch(handleError);
});
@ -122,7 +120,7 @@ const addCommand = (command: Command, config: ConfigT) => {
opt.command,
opt.description,
opt.parse || defaultOptParser,
typeof opt.default === 'function' ? opt.default(config) : opt.default,
typeof opt.default === 'function' ? opt.default(cfg) : opt.default,
));
// Placeholder option for --config, which is parsed before any other option,
@ -130,24 +128,6 @@ const addCommand = (command: Command, config: ConfigT) => {
cmd.option('--config [string]', 'Path to the CLI configuration file');
};
function getCliConfig() {
// Use a lightweight option parser to look up the CLI configuration file,
// which we need to set up the parser for the other args and options
const cliArgs = minimist(process.argv.slice(2));
let cwd;
let configPath;
if (cliArgs.config != null) {
cwd = process.cwd();
configPath = cliArgs.config;
} else {
cwd = __dirname;
configPath = Config.findConfigPath(cwd);
}
return Config.get(cwd, defaultConfig, configPath);
}
function run() {
const setupEnvScript = /^win/.test(process.platform)
? 'setup_env.bat'
@ -155,7 +135,6 @@ function run() {
childProcess.execFileSync(path.join(__dirname, setupEnvScript));
const config = getCliConfig();
commands.forEach(cmd => addCommand(cmd, config));
commander.parse(process.argv);

View File

@ -10,11 +10,11 @@
*/
'use strict';
const getUserCommands = require('./core/getCommands');
const { getProjectCommands } = require('./core');
import type {ConfigT} from './util/Config';
import type { ConfigT } from './core';
export type Command = {
export type CommandT = {
name: string,
description?: string,
usage?: string,
@ -66,10 +66,10 @@ const undocumentedCommands = [
},
];
const commands: Array<Command> = [
const commands: Array<CommandT> = [
...documentedCommands,
...undocumentedCommands,
...getUserCommands(),
...getProjectCommands(),
];
module.exports = commands;

View File

@ -1,6 +1,6 @@
jest.autoMockOff();
const findAndroidAppFolder = require('../../config/android/findAndroidAppFolder');
const findAndroidAppFolder = require('../../android/findAndroidAppFolder');
const mockFs = require('mock-fs');
const mocks = require('../../__fixtures__/android');

View File

@ -1,6 +1,6 @@
jest.autoMockOff();
const findManifest = require('../../config/android/findManifest');
const findManifest = require('../../android/findManifest');
const mockFs = require('mock-fs');
const mocks = require('../../__fixtures__/android');

View File

@ -1,6 +1,6 @@
jest.autoMockOff();
const findPackageClassName = require('../../config/android/findPackageClassName');
const findPackageClassName = require('../../android/findPackageClassName');
const mockFs = require('mock-fs');
const mocks = require('../../__fixtures__/android');

View File

@ -1,6 +1,6 @@
jest.autoMockOff();
const getDependencyConfig = require('../../config/android').dependencyConfig;
const getDependencyConfig = require('../../android').dependencyConfig;
const mockFs = require('mock-fs');
const mocks = require('../../__fixtures__/android');
const userConfig = {};

View File

@ -1,6 +1,6 @@
jest.autoMockOff();
const getProjectConfig = require('../../config/android').projectConfig;
const getProjectConfig = require('../../android').projectConfig;
const mockFs = require('mock-fs');
const mocks = require('../../__fixtures__/android');

View File

@ -1,7 +1,7 @@
jest.autoMockOff();
const findManifest = require('../../config/android/findManifest');
const readManifest = require('../../config/android/readManifest');
const findManifest = require('../../android/findManifest');
const readManifest = require('../../android/readManifest');
const mockFs = require('mock-fs');
const mocks = require('../../__fixtures__/android');

View File

@ -1,6 +1,6 @@
jest.autoMockOff();
const findAssets = require('../config/findAssets');
const findAssets = require('../findAssets');
const mockFs = require('mock-fs');
const dependencies = require('../__fixtures__/dependencies');
const isArray = (arg) =>

View File

@ -1,6 +1,6 @@
jest.autoMockOff();
const findProject = require('../../config/ios/findProject');
const findProject = require('../../ios/findProject');
const mockFs = require('mock-fs');
const projects = require('../../__fixtures__/projects');
const ios = require('../../__fixtures__/ios');

View File

@ -1,6 +1,6 @@
jest.autoMockOff();
const getProjectConfig = require('../../config/ios').projectConfig;
const getProjectConfig = require('../../ios').projectConfig;
const mockFs = require('mock-fs');
const projects = require('../../__fixtures__/projects');

View File

@ -1,56 +0,0 @@
/**
* 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 android = require('./android');
const findAssets = require('./findAssets');
const ios = require('./ios');
const windows = require('./windows');
const path = require('path');
const wrapCommands = require('./wrapCommands');
const getRNPMConfig = (folder) =>
require(path.join(folder, './package.json')).rnpm || {};
/**
* Returns project config from the current working directory
* @return {Object}
*/
exports.getProjectConfig = function getProjectConfig() {
const folder = process.cwd();
const rnpm = getRNPMConfig(folder);
return Object.assign({}, rnpm, {
ios: ios.projectConfig(folder, rnpm.ios || {}),
android: android.projectConfig(folder, rnpm.android || {}),
windows: windows.projectConfig(folder, rnpm.windows || {}),
assets: findAssets(folder, rnpm.assets),
});
};
/**
* Returns a dependency config from node_modules/<package_name>
* @param {String} packageName Dependency name
* @return {Object}
*/
exports.getDependencyConfig = function getDependencyConfig(packageName) {
const folder = path.join(process.cwd(), 'node_modules', packageName);
const rnpm = getRNPMConfig(
path.join(process.cwd(), 'node_modules', packageName)
);
return Object.assign({}, rnpm, {
ios: ios.dependencyConfig(folder, rnpm.ios || {}),
android: android.dependencyConfig(folder, rnpm.android || {}),
windows: windows.dependencyConfig(folder, rnpm.windows || {}),
assets: findAssets(folder, rnpm.assets),
commands: wrapCommands(rnpm.commands),
params: rnpm.params || [],
});
};

View File

@ -0,0 +1,115 @@
/**
* 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.
*
* @flow
*/
'use strict';
const path = require('path');
const flatten = require('lodash').flatten;
const blacklist = require('../../packager/blacklist');
const android = require('./android');
const findAssets = require('./findAssets');
const ios = require('./ios');
const windows = require('./windows');
const wrapCommands = require('./wrapCommands');
const findPlugins = require('./findPlugins');
import type {ConfigT} from './index';
const getRNPMConfig = (folder) =>
// $FlowFixMe non-literal require
require(path.join(folder, './package.json')).rnpm || {};
const attachPackage = (command, pkg) => Array.isArray(command)
? command.map(cmd => attachPackage(cmd, pkg))
: { ...command, pkg };
/**
* Default configuration for the CLI.
*
* If you need to override any of this functions do so by defining the file
* `rn-cli.config.js` on the root of your project with the functions you need
* to tweak.
*/
const config: ConfigT = {
getProjectCommands() {
const appRoot = process.cwd();
const plugins = findPlugins([appRoot])
.map(pathToCommands => {
const name = pathToCommands.split(path.sep)[0];
return attachPackage(
// $FlowFixMe non-literal require
require(path.join(appRoot, 'node_modules', pathToCommands)),
// $FlowFixMe non-literal require
require(path.join(appRoot, 'node_modules', name, 'package.json'))
);
});
return flatten(plugins);
},
getProjectConfig() {
const folder = process.cwd();
const rnpm = getRNPMConfig(folder);
return Object.assign({}, rnpm, {
ios: ios.projectConfig(folder, rnpm.ios || {}),
android: android.projectConfig(folder, rnpm.android || {}),
windows: windows.projectConfig(folder, rnpm.windows || {}),
assets: findAssets(folder, rnpm.assets),
});
},
getDependencyConfig(packageName) {
const folder = path.join(process.cwd(), 'node_modules', packageName);
const rnpm = getRNPMConfig(
path.join(process.cwd(), 'node_modules', packageName)
);
return Object.assign({}, rnpm, {
ios: ios.dependencyConfig(folder, rnpm.ios || {}),
android: android.dependencyConfig(folder, rnpm.android || {}),
windows: windows.dependencyConfig(folder, rnpm.windows || {}),
assets: findAssets(folder, rnpm.assets),
commands: wrapCommands(rnpm.commands),
params: rnpm.params || [],
});
},
getAssetExts() {
return [];
},
getPlatforms() {
return [];
},
getBlacklistRE() {
return blacklist();
},
getTransformModulePath() {
return require.resolve('../../packager/transformer');
},
getProjectRoots() {
const root = process.env.REACT_NATIVE_APP_ROOT;
if (root) {
return [path.resolve(root)];
}
if (__dirname.match(/node_modules[\/\\]react-native[\/\\]local-cli[\/\\]core$/)) {
// Packager is running from node_modules.
// This is the default case for all projects created using 'react-native init'.
return [path.resolve(__dirname, '../../../..')];
} else if (__dirname.match(/Pods[\/\\]React[\/\\]packager$/)) {
// React Native was installed using CocoaPods.
return [path.resolve(__dirname, '../../../..')];
} else {
return [path.resolve(__dirname, '../..')];
}
},
};
module.exports = config;

View File

@ -1,35 +0,0 @@
/**
* 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 findPlugins = require('./findPlugins');
const path = require('path');
const flatten = require('lodash').flatten;
const attachPackage = (command, pkg) => Array.isArray(command)
? command.map(cmd => attachPackage(cmd, pkg))
: { ...command, pkg };
/**
* @return {Array} Array of commands
*/
module.exports = function getCommands() {
const appRoot = process.cwd();
const plugins = findPlugins([appRoot])
.map(pathToCommands => {
const name = pathToCommands.split(path.sep)[0];
return attachPackage(
require(path.join(appRoot, 'node_modules', pathToCommands)),
require(path.join(appRoot, 'node_modules', name, 'package.json'))
);
});
return flatten(plugins);
};

85
local-cli/core/index.js Normal file
View File

@ -0,0 +1,85 @@
/**
* 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.
*
* @flow
*/
'use strict';
const Config = require('../util/Config');
const defaultConfig = require('./default.config');
const minimist = require('minimist');
import type {GetTransformOptions} from '../../packager/react-packager/src/Bundler';
import type {CommandT} from '../commands';
/**
* Configuration file of the CLI.
*/
export type ConfigT = {
extraNodeModules?: { [id: string]: string },
/**
* Specify any additional asset extentions to be used by the packager.
* For example, if you want to include a .ttf file, you would return ['ttf']
* from here and use `require('./fonts/example.ttf')` inside your app.
*/
getAssetExts?: () => Array<string>,
/**
* Specify any additional platforms to be used by the packager.
* For example, if you want to add a "custom" platform, and use modules
* ending in .custom.js, you would return ['custom'] here.
*/
getPlatforms: () => Array<string>,
/**
* Returns the path to a custom transformer. This can also be overridden
* with the --transformer commandline argument.
*/
getTransformModulePath?: () => string,
getTransformOptions?: GetTransformOptions,
transformVariants?: () => {[name: string]: Object},
/**
* Returns a regular expression for modules that should be ignored by the
* packager on a given platform.
*/
getBlacklistRE(): RegExp,
getProjectRoots(): Array<string>,
getAssetExts(): Array<string>,
/**
* Returns an array of project commands used by the CLI to load
*/
getProjectCommands(): Array<CommandT>,
/**
* Returns project config from the current working directory
*/
getProjectConfig(): Object,
/**
* Returns dependency config from <node_modules>/packageName
*/
getDependencyConfig(pkgName: string): Object,
};
/**
* Loads the CLI configuration
*/
function getCliConfig(): ConfigT {
const cliArgs = minimist(process.argv.slice(2));
let cwd;
let configPath;
if (cliArgs.config != null) {
cwd = process.cwd();
configPath = cliArgs.config;
} else {
cwd = __dirname;
configPath = Config.findConfigPath(cwd);
}
return Config.get(cwd, defaultConfig, configPath);
}
module.exports = getCliConfig();

View File

@ -8,7 +8,7 @@
*/
'use strict';
const makeCommand = require('../makeCommand');
const makeCommand = require('./makeCommand');
module.exports = function wrapCommands(commands) {
const mappedCommands = {};

View File

@ -1,80 +0,0 @@
/**
* 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';
var blacklist = require('../packager/blacklist');
var path = require('path');
var rnpmConfig = require('./core/config');
/**
* Default configuration for the CLI.
*
* If you need to override any of this functions do so by defining the file
* `rn-cli.config.js` on the root of your project with the functions you need
* to tweak.
*/
var config = {
getProjectRoots,
getProjectConfig: rnpmConfig.getProjectConfig,
getDependencyConfig: rnpmConfig.getDependencyConfig,
/**
* Specify any additional asset extentions to be used by the packager.
* For example, if you want to include a .ttf file, you would return ['ttf']
* from here and use `require('./fonts/example.ttf')` inside your app.
*/
getAssetExts() {
return [];
},
/**
* Specify any additional platforms to be used by the packager.
* For example, if you want to add a "custom" platform, and use modules
* ending in .custom.js, you would return ['custom'] here.
*/
getPlatforms() {
return [];
},
/**
* Returns a regular expression for modules that should be ignored by the
* packager on a given platform.
*/
getBlacklistRE() {
return blacklist();
},
/**
* Returns the path to a custom transformer. This can also be overridden
* with the --transformer commandline argument.
*/
getTransformModulePath() {
return require.resolve('../packager/transformer');
},
};
function getProjectRoots() {
var root = process.env.REACT_NATIVE_APP_ROOT;
if (root) {
return [path.resolve(root)];
}
if (__dirname.match(/node_modules[\/\\]react-native[\/\\]local-cli$/)) {
// Packager is running from node_modules.
// This is the default case for all projects created using 'react-native init'.
return [path.resolve(__dirname, '../../..')];
} else if (__dirname.match(/Pods[\/\\]React[\/\\]packager$/)) {
// React Native was installed using CocoaPods.
return [path.resolve(__dirname, '../../..')];
} else {
return [path.resolve(__dirname, '..')];
}
}
module.exports = config;

View File

@ -14,37 +14,25 @@ const assert = require('assert');
const fs = require('fs');
const path = require('path');
import type {GetTransformOptions} from '../../packager/react-packager/src/Bundler/index.js';
const RN_CLI_CONFIG = 'rn-cli.config.js';
export type ConfigT = {
extraNodeModules?: {[id: string]: string},
getAssetExts?: () => Array<string>,
getTransformModulePath?: () => string,
getTransformOptions?: GetTransformOptions,
transformVariants?: () => {[name: string]: Object},
getBlacklistRE(): RegExp,
getProjectRoots(): Array<string>,
};
// TODO: @bestander & @grabbou - get rid when internal tests are fixed
export type { ConfigT } from '../core';
/**
* Module capable of getting the configuration that should be used for
* the `rn-cli`. The configuration file is a JS file named `rn-cli.config.js`.
* It has to be on any parent directory of the cli.
* Module capable of getting the configuration out of a given file.
*
* The function will return all the default configuration functions overriden
* by those found on `rn-cli.config.js`, if any. If no default config is
* provided and no configuration can be found in the directory hierarchy an
* error will be thrown.
* The function will return all the default configuration, as specified by the
* `defaultConfig` param overriden by those found on `rn-cli.config.js` files, if any. If no
* default config is provided and no configuration can be found in the directory
* hierarchy, an error will be thrown.
*/
const Config = {
get(
get<T>(
cwd: string,
defaultConfig?: ConfigT | null,
defaultConfig?: T | null,
pathToConfig?: string | null,
): ConfigT {
): T {
let baseConfig;
// Handle the legacy code path where pathToConfig is unspecified
@ -67,6 +55,7 @@ const Config = {
require(path.join(cwd, pathToConfig));
}
// $FlowFixMe we return `at least` T + extras
return {
...defaultConfig,
...baseConfig,