File: development-for-libraries.md | Updated: 11/15/2025
Hide navigation
Search
Ctrl K
Home Guides EAS Reference Learn
Archive Expo Snack Discord and Forums Newsletter
Copy page
Learn how to develop config plugins for Expo and React Native libraries.
Copy page
Expo config plugins in a React Native library represent a transformative approach to automating native project configuration. Rather than requiring library users to manually edit native files, such as AndroidManifest.xml, Info.plist, and so on, you can provide a plugin that handles these configurations automatically during the prebuild process. This changes developer experience from error-prone manual setup to reliable, automated configuration that can work consistently across different projects.
This guide explains key configuration steps and strategies that you can use to implement a config plugin in your library.
Strategic value of a config plugin in a library
Config plugins tend to solve interconnected problems that have historically made React Native library adoption more difficult than it should be. At times, when a user installs a React Native library, they face a complex set of native configuration steps that must be performed correctly for the library to function. These steps are platform-specific and sometimes require deep knowledge of native development concepts.
By creating a config plugin within your library, you can transform this complex-looking manual process into a simple configuration declaration that a user can apply in their Expo project's app config file (usually, app.json). This reduces the barrier to adoption for your library and simultaneously makes the setup process reliable.
Beyond immediate user experience improvements, config plugins enable compatibility with Continuous Native Generation , where native directories are generated automatically rather than checked into version control. Without a config plugin, developers who have adopted CNG face a difficult choice: either abandon the CNG workflow to manually configure native files, or invest significant effort in creating their own automation solutions. This creates a substantial barrier to library adoption in modern Expo development workflows.
A directory structure is the foundation for maintaining config plugins within your library. Below is an example directory structure:
.
āandroid``Android native module code
āāsrc
āāāmain
āāāājava
āāāāācom
āāāāāāyour-awesome-library
āābuild.gradle
āios``iOS native module code
āāYourAwesomeLibrary
āāYourAwesomeLibrary.podspec
āsrc
āāindex.ts``Main library entry point
āāYourAwesomeLibrary.ts``Core library implementation
āātypes.ts``TypeScript type definitions
āplugin
āāsrc
āāāindex.ts``Plugin entry point
āāāwithAndroid.ts``Android-specific configurations
āāāwithIos.ts``iOS-specific configurations
āābuild
āā__tests__
āātsconfig.json``Plugin-specific TypeScript config
āexample
āāapp.json``Example app configuration
āāApp.tsx``Example app implementation
āāpackage.json``Example app dependencies
ā__tests__
āapp.plugin.js``Plugin entry point for Expo CLI
āpackage.json``Package configuration
ātsconfig.json``Main TypeScript configuration
ājest.config.js``Testing configuration
āREADME.md``Documentation
The directory structure example above highlights the following organizational principles:
Installation and configuration for development
The most straightforward approach to leverage Expo's tooling is to use expo and expo-module-scripts
.
expo provides a config plugin API and types that your plugin will use.expo-module-scripts provides build tooling specifically designed for Expo modules and config plugins. It also handles TypeScript compilation.Terminal
Copy
-Ā npx expo install package
When using expo-module-scripts, it requires the following package.json configuration. For any already existing script with the same script name, replace it.
package.json
Copy
{ "scripts": { "build": "expo-module build", "build:plugin": "expo-module build plugin", "clean": "expo-module clean", "test": "expo-module test", "prepare": "expo-module prepare", "prepublishOnly": "expo-module prepublishOnly" }, "devDependencies": { "expo": "^54.0.0" }, "peerDependencies": { "expo": ">=54.0.0" }, "peerDependenciesMeta": { "expo": { "optional": true } } }
Show More
The next step is to add TypeScript support within the plugins directory. Open plugins/tsconfig.json file and add the following:
plugins/tsconfig.json
Copy
{ "extends": "expo-module-scripts/tsconfig.plugin", "compilerOptions": { "outDir": "build", "rootDir": "src" }, "include": ["./src"], "exclude": ["**/__mocks__/*", "**/__tests__/*"] }
You also need to define the main entry point for your config plugin in the app.plugin.js file, which exports the compiled plugin code from the plugin/build directory:
app.plugin.js
Copy
module.exports = require('./plugin/build');
The above configuration is essential because when the Expo CLI looks for a plugin, it checks for this file in the project root of your library. The plugin/build directory contains the JavaScript files generated from your config plugin's TypeScript source code.
Essential patterns for a successful config plugin implementation include:
Every config plugin follows the same pattern: receives configuration and parameters, applies transformations through mods, and returns the modified configuration. Consider the following core plugin structure looks like:
Index file
Android
iOS
plugin/src/index.ts
Copy
import { type ConfigPlugin, withAndroidManifest, withInfoPlist } from 'expo/config-plugins'; export interface YourLibraryPluginProps { customProperty?: string; enableFeature?: boolean; } const withYourLibrary: ConfigPlugin<YourLibraryPluginProps> = (config, props = {}) => { // Apply Android configurations config = withAndroidConfiguration(config, props); // Apply iOS configurations config = withIosConfiguration(config, props); return config; }; export default withYourLibrary;
plugin/src/withAndroid.ts
Copy
import { type ConfigPlugin, withAndroidManifest, AndroidConfig } from 'expo/config-plugins'; export const withAndroidConfiguration: ConfigPlugin<YourLibraryPluginProps> = (config, props) => { return withAndroidManifest(config, config => { const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults); AndroidConfig.Manifest.addMetaDataItemToMainApplication( mainApplication, 'your_library_config_key', props.customProperty || 'default_value' ); return config; }); };
plugin/src/withIos.ts
Copy
import { type ConfigPlugin, withInfoPlist } from 'expo/config-plugins'; export const withIosConfiguration: ConfigPlugin<YourLibraryPluginProps> = (config, props) => { return withInfoPlist(config, config => { config.modResults.YourLibraryCustomProperty = props.customProperty || 'default_value'; if (props.enableFeature) { config.modResults.YourLibraryFeatureEnabled = true; } return config; }); };
Config plugin testing differs from regular library testing because you are testing configuration transformations rather than runtime behavior. Your plugin receives configuration objects and returns modified configuration objects.
Effective testing for a config plugin can be a combination of one or more of the following:
Since unit tests focus on a plugin's transformation logic without involving the file system, you can use Jest to create and run mock configuration objects, pass them through your plugin, and verify expected modifications are made correctly. For example:
plugin/__tests__/withYourLibrary.test.ts
Copy
import { withYourLibrary } from '../src'; describe('withYourLibrary', () => { it('should configure Android with custom property', () => { const config = { name: 'test-app', slug: 'test-app', platforms: ['android', 'ios'], }; const result = withYourLibrary(config, { customProperty: 'test-value', }); // Verify the plugin was applied correctly expect(result.plugins).toBeDefined(); }); });
Show More
Errors should be handled gracefully inside your config plugin to provide clear feedback when a configuration fails. Use try-catch blocks to intercept errors early:
plugin/src/index.ts
Copy
const withYourLibrary: ConfigPlugin<YourLibraryPluginProps> = (config, props = {}) => { try { // Validate configuration early validateProps(props); // Apply configurations config = withAndroidConfiguration(config, props); config = withIosConfiguration(config, props); return config; } catch (error) { // Re-throw with more context if needed throw new Error(`Failed to configure YourLibrary plugin: ${error.message}`); } };
If your library doesn't use expo-module-scripts, you have two options:
For libraries using different build tools (like those created with create-react-native-library), add an app.plugin.js file and build it along with your main package:
app.plugin.js
Copy
module.exports = require('./lib/plugin');
Some libraries distribute their config plugin as a separate package from their main library. This approach allows you to maintain your config plugin separately from the rest of your native module. You need to include export in app.plugin.js and compile the build directory from your plugin.
app.plugin.js
Copy
{ "name": "your-library-expo-plugin", "main": "app.plugin.js", "files": ["app.plugin.js", "build/"], "peerDependencies": { "expo": "*", "your-library": "*" } }
Plugin development best practices
npx expo prebuild without the --clean flag to sync changes to the config, rather than recreating the native project entirely. This may be more difficult with dangerous mods.withFeatureName for the plugin function name if it applies to all platforms. If the plugin is platform-specific, use a camel case naming with the platform right after "with". For example, withAndroidSplash, withIosSplash.withAndroidSplash, withIosSplash. This makes using the --platform flag in npx expo prebuild a bit easier to follow in EXPO_DEBUG mode, as the logging will show which platform-specific functions are being executed.memfs
), you can see examples of this in the expo-notifications
plugin tests.
expo-module-scripts plugin
tooling for more info.sdkVersion via a config plugin, this can break commands like expo install and cause other unexpected issues.