/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
const cosmiconfig = require('cosmiconfig');
const getDefaultConfig = require('./defaults');
const {dirname, resolve, join} = require('path');
import type {ConfigT, InputConfigT} from './configTypes.flow';
type CosmiConfigResult = {
filepath: string,
isEmpty: boolean,
config: (ConfigT => Promise<ConfigT>) | (ConfigT => ConfigT) | InputConfigT,
};
type YargArguments = {
config?: string,
cwd?: string,
port?: string | number,
host?: string,
projectRoot?: string,
watchFolders?: Array<string>,
assetExts?: Array<string>,
sourceExts?: Array<string>,
platforms?: Array<string>,
providesModuleNodeModules?: Array<string>,
'max-workers'?: string | number,
maxWorkers?: string | number,
transformer?: string,
'reset-cache'?: boolean,
resetCache?: boolean,
verbose?: boolean,
};
/**
* Takes the last argument if multiple of the same argument are given
*/
function overrideArgument<T>(arg: Array<T> | T): T {
if (arg == null) {
return arg;
}
if (Array.isArray(arg)) {
return arg[arg.length - 1];
}
return arg;
}
const explorer = cosmiconfig('metro', {
searchPlaces: [
'metro.config.js',
'metro.config.json',
'package.json',
'rn-cli.config.js',
],
loaders: {
'.json': cosmiconfig.loadJson,
'.yaml': cosmiconfig.loadYaml,
'.yml': cosmiconfig.loadYaml,
'.js': cosmiconfig.loadJs,
'.es6': cosmiconfig.loadJs,
noExt: cosmiconfig.loadYaml,
},
});
async function resolveConfig(
path?: string,
cwd?: string,
): Promise<CosmiConfigResult> {
if (path) {
return explorer.load(path);
}
const result = await explorer.search(cwd);
if (result == null) {
// No config file found, return a default
return {
isEmpty: true,
filepath: join(cwd || process.cwd(), 'metro.config.stub.js'),
config: {},
};
}
return result;
}
function mergeConfig<T: InputConfigT>(
defaultConfig: T,
...configs: Array<InputConfigT>
): T {
// If the file is a plain object we merge the file with the default config,
// for the function we don't do this since that's the responsibility of the user
return configs.reduce(
(totalConfig, nextConfig) => ({
...totalConfig,
...nextConfig,
resolver: {
...totalConfig.resolver,
...(nextConfig.resolver || {}),
},
serializer: {
...totalConfig.serializer,
...(nextConfig.serializer || {}),
},
transformer: {
...totalConfig.transformer,
...(nextConfig.transformer || {}),
},
server: {
...totalConfig.server,
...(nextConfig.server || {}),
},
}),
defaultConfig,
);
}
async function loadMetroConfigFromDisk(
path?: string,
cwd?: string,
defaultConfigOverrides: InputConfigT,
): Promise<ConfigT> {
const resolvedConfigResults: CosmiConfigResult = await resolveConfig(
path,
cwd,
);
const {config: configModule, filepath} = resolvedConfigResults;
const rootPath = dirname(filepath);
const defaultConfig: ConfigT = await getDefaultConfig(rootPath);
if (typeof configModule === 'function') {
// Get a default configuration based on what we know, which we in turn can pass
// to the function.
const resultedConfig = await configModule(defaultConfig);
return resultedConfig;
}
return mergeConfig(defaultConfig, defaultConfigOverrides, configModule);
}
function overrideConfigWithArguments(
config: ConfigT,
argv: YargArguments,
): ConfigT {
// We override some config arguments here with the argv
const output: InputConfigT = {
resolver: {},
serializer: {},
server: {},
transformer: {},
};
if (argv.port != null) {
output.server.port = Number(argv.port);
}
if (argv.projectRoot != null) {
output.projectRoot = argv.projectRoot;
}
if (argv.watchFolders != null) {
output.watchFolders = argv.watchFolders;
}
if (argv.assetExts != null) {
output.resolver.assetExts = argv.assetExts;
}
if (argv.sourceExts != null) {
output.resolver.sourceExts = argv.sourceExts;
}
if (argv.platforms != null) {
output.resolver.platforms = argv.platforms;
}
if (argv.providesModuleNodeModules != null) {
output.resolver.providesModuleNodeModules = argv.providesModuleNodeModules;
}
if (argv['max-workers'] != null || argv.maxWorkers != null) {
output.maxWorkers = Number(argv['max-workers'] || argv.maxWorkers);
}
if (argv.transformer != null) {
output.transformer.babelTransformerPath = resolve(argv.transformer);
}
if (argv['reset-cache'] != null) {
output.resetCache = argv['reset-cache'];
}
if (argv.resetCache != null) {
output.resetCache = argv.resetCache;
}
if (argv.verbose === false) {
output.reporter = {update: () => {}};
// TODO: Ask if this is the way to go
}
return mergeConfig(config, output);
}
/**
* Load the metro configuration from disk
* @param {object} argv Arguments coming from the CLI, can be empty
* @param {object} defaultConfigOverrides A configuration that can override the default config
* @return {object} Configuration returned
*/
async function loadConfig(
argv: YargArguments = {},
defaultConfigOverrides: InputConfigT = {},
): Promise<ConfigT> {
argv.config = overrideArgument(argv.config);
const configuration = await loadMetroConfigFromDisk(
argv.config,
argv.cwd,
defaultConfigOverrides,
);
// Override the configuration with cli parameters
const configWithArgs = overrideConfigWithArguments(configuration, argv);
const overriddenConfig = {};
// The resolver breaks if "json" is missing from `resolver.sourceExts`
const sourceExts = configWithArgs.resolver.sourceExts;
if (!configWithArgs.resolver.sourceExts.includes('json')) {
overriddenConfig.resolver = {
sourceExts: [...sourceExts, 'json'],
};
}
overriddenConfig.watchFolders = [
configWithArgs.projectRoot,
...configWithArgs.watchFolders,
];
// Set the watchfolders to include the projectRoot, as Metro assumes that is
// the case
return mergeConfig(configWithArgs, overriddenConfig);
}
module.exports = {
loadConfig,
resolveConfig,
mergeConfig,
};