Config parser and resolver for Metro bundler with support for loading, merging, and validating configuration files.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Deep merge multiple Metro configurations with validation and type safety.
Deeply merges multiple Metro configuration objects with intelligent handling of arrays, objects, and functions.
/**
* Deep merges multiple Metro configuration objects
* @param defaultConfig - Base configuration object
* @param configs - Additional configurations to merge (later configs take precedence)
* @returns Complete merged Metro configuration
*/
function mergeConfig<T: $ReadOnly<InputConfigT>>(
defaultConfig: T,
...configs: Array<InputConfigT>
): T;Usage Examples:
import { getDefaultConfig, loadConfig, mergeConfig } from "metro-config";
// Merge user config with defaults
const defaultConfig = await getDefaultConfig();
const userConfig = await loadConfig();
const finalConfig = mergeConfig(defaultConfig, userConfig);
// Merge multiple configurations
const baseConfig = {
resolver: {
sourceExts: ['js', 'jsx'],
platforms: ['ios', 'android']
},
server: {
port: 8081
}
};
const platformConfig = {
resolver: {
sourceExts: ['js', 'jsx', 'ts', 'tsx'], // Replaces array
assetExts: ['png', 'jpg'] // Adds new property
}
};
const customConfig = {
transformer: {
babelTransformerPath: 'custom-transformer'
}
};
// Merge multiple configs - later configs take precedence
const merged = mergeConfig(baseConfig, platformConfig, customConfig);
// Result:
// {
// resolver: {
// sourceExts: ['js', 'jsx', 'ts', 'tsx'],
// platforms: ['ios', 'android'],
// assetExts: ['png', 'jpg']
// },
// server: {
// port: 8081
// },
// transformer: {
// babelTransformerPath: 'custom-transformer'
// }
// }Objects are deeply merged, with properties from the second config taking precedence:
const config1 = {
resolver: {
sourceExts: ['js'],
platforms: ['ios'],
blockList: /node_modules/
}
};
const config2 = {
resolver: {
sourceExts: ['js', 'ts'], // Overrides
assetExts: ['png'] // Adds new property
// platforms and blockList preserved from config1
}
};
const merged = mergeConfig(config1, config2);
// resolver.sourceExts = ['js', 'ts']
// resolver.platforms = ['ios'] (preserved)
// resolver.assetExts = ['png'] (added)
// resolver.blockList = /node_modules/ (preserved)Arrays from the second configuration completely replace arrays from the first:
const config1 = {
resolver: {
sourceExts: ['js', 'jsx']
}
};
const config2 = {
resolver: {
sourceExts: ['ts', 'tsx'] // Completely replaces
}
};
const merged = mergeConfig(config1, config2);
// resolver.sourceExts = ['ts', 'tsx'] (not merged)Functions from the second configuration replace functions from the first:
const config1 = {
transformer: {
getTransformOptions: async () => ({
transform: { inlineRequires: false }
})
}
};
const config2 = {
transformer: {
getTransformOptions: async () => ({
transform: { inlineRequires: true }
})
}
};
const merged = mergeConfig(config1, config2);
// Uses config2's getTransformOptions functionPrimitive values (strings, numbers, booleans) from the second config override the first:
const config1 = {
server: {
port: 8081,
forwardClientLogs: false
}
};
const config2 = {
server: {
port: 3000 // Overrides
// forwardClientLogs preserved from config1
}
};
const merged = mergeConfig(config1, config2);
// server.port = 3000
// server.forwardClientLogs = false (preserved)To extend rather than replace arrays, pre-process your configuration:
import { getDefaultConfig, mergeConfig } from "metro-config";
async function createExtendedConfig(userConfig) {
const defaultConfig = await getDefaultConfig();
// Extend sourceExts rather than replace
const extendedConfig = {
...userConfig,
resolver: {
...userConfig.resolver,
sourceExts: [
...defaultConfig.resolver.sourceExts,
...(userConfig.resolver?.sourceExts || [])
]
}
};
return mergeConfig(defaultConfig, extendedConfig);
}Handle environment-specific configurations:
import { getDefaultConfig, mergeConfig } from "metro-config";
async function createConfig() {
const baseConfig = await getDefaultConfig();
const developmentConfig = {
server: {
port: 8081,
forwardClientLogs: true
},
resetCache: true
};
const productionConfig = {
transformer: {
minifierPath: 'metro-minify-terser',
minifierConfig: {
mangle: { toplevel: true },
compress: { drop_console: true }
}
}
};
const envConfig = process.env.NODE_ENV === 'production'
? productionConfig
: developmentConfig;
return mergeConfig(baseConfig, envConfig);
}Compose transform functions when merging:
const baseConfig = {
transformer: {
getTransformOptions: async (entryPoints, options) => ({
transform: {
experimentalImportSupport: false,
inlineRequires: false
}
})
}
};
const enhancedConfig = {
transformer: {
getTransformOptions: async (entryPoints, options) => {
// Get base transform options
const baseTransform = await baseConfig.transformer.getTransformOptions(
entryPoints,
options
);
// Enhance with additional options
return {
...baseTransform,
transform: {
...baseTransform.transform,
inlineRequires: true // Override specific option
},
preloadedModules: false
};
}
}
};
const merged = mergeConfig(baseConfig, enhancedConfig);The merged configuration is automatically validated:
import { mergeConfig } from "metro-config";
try {
const merged = mergeConfig(config1, config2);
// Configuration is valid and ready to use
} catch (error) {
if (error.name === 'ValidationError') {
console.error('Merged configuration is invalid:', error.message);
// Handle validation errors
}
}When using TypeScript, the merge function provides type safety:
import type { InputConfigT } from 'metro-config';
import { mergeConfig } from 'metro-config';
const config1: InputConfigT = {
resolver: {
sourceExts: ['js', 'jsx']
}
};
const config2: InputConfigT = {
resolver: {
sourceExts: ['ts', 'tsx'],
// TypeScript ensures all properties are valid
}
};
const merged = mergeConfig(config1, config2);
// merged has complete ConfigT type