Edit this page
Learn about what are plugins and mods when creating a config plugin.
Edit this page
Plugins are synchronous functions that accept an ExpoConfig
and return a modified ExpoConfig
.
with<Plugin Functionality>
, for example, withFacebook
.mods
that are added.plugins
are always invoked when the config is read by the expo/config
method getConfig
. However, the mods
are only invoked during the "syncing" phase of npx expo prebuild
.Here is an example of the most basic config plugin:
const withNothing = config => config;
Say you wanted to create a plugin that added custom values to Info.plist in an iOS project:
const withMySDK = (config, { apiKey }) => {
if (!config.ios) {
config.ios = {};
}
if (!config.ios.infoPlist) {
config.ios.infoPlist = {};
}
config.ios.infoPlist['MY_CUSTOM_NATIVE_IOS_API_KEY'] = apiKey;
return config;
};
module.exports.withMySDK = withMySDK;
To use the plugin, import it and wrap the config:
const { withMySDK } = require('./my-plugin');
const config = {
name: 'my app',
};
module.exports = withMySDK(config, { apiKey: 'X-XXX-XXX' });
You may want to create a plugin in a different file, here's how:
ConfigPlugin
type.import/export
keywords, use module.exports
in the shipped plugin file.app.config
file, anything more would require a bundler which would add too many "opinions" for a config file.Consider the following example that changes the config name:
app.config.js
Expo config
my-plugin.js
Custom Config Plugin file
module.exports = function withPrefixedName(config, prefix) {
// Modify the config
config.name = prefix + '-' + config.name;
// Return the results
return config;
};
{
"name": "my-app",
"plugins": [["./my-plugin", "custom"]]
}
It evaluates to the following JSON config:
{
"name": "custom-my-app",
"plugins": [["./my-plugin", "custom"]]
}
Once you add a few plugins, your app.config.js code can become difficult to read and manipulate. To combat this, expo/config-plugins
provides a withPlugins
function which can be used to chain plugins together and execute them in order.
/// Create a config
const config = {
name: 'my app',
};
// ❌ Hard to read
withDelta(withFoo(withBar(config, 'input 1'), 'input 2'), 'input 3');
// ✅ Easy to read
import { withPlugins } from 'expo/config-plugins';
withPlugins(config, [
[withBar, 'input 1'],
[withFoo, 'input 2'],
// When no input is required, you can just pass the method...
withDelta,
]);
To support JSON configs, we also added the plugins
array which just uses withPlugins
under the hood.
Here is the same config as above, but even simpler:
export default {
name: 'my app',
plugins: [
[withBar, 'input 1'],
[withFoo, 'input 2'],
[withDelta, 'input 3'],
],
};
A modifier (mod for short) is an async function that accepts a config and a data object, then manipulates and returns both as an object.
Mods are added to the mods
object of the app config. The mods
object is different from the rest of the app config because it doesn't get serialized
after the initial reading, which means you can use it to perform actions during code generation.
If possible, you should attempt to use basic plugins instead of mods, as they're simpler to work with.
mods
are omitted from the manifest and cannot be accessed via Updates.manifest
. Mods exist for the sole purpose of modifying native project files during code generation!mods
can be used to read and write files safely during the npx expo prebuild
command. This is how Expo CLI modifies the Info.plist, entitlements, xcproj, and so on.mods
are platform-specific and should always be added to a platform-specific object:module.exports = {
name: 'my-app',
mods: {
ios: {
/* iOS mods... */
},
android: {
/* Android mods... */
},
},
};
getPrebuildConfig
from @expo/prebuild-config
.withIosExpoPlugins
. This includes name, version, icons, locales, and so on.compileModsAsync
mods.ios.infoPlist
), then writing the results to the file system.projectRoot
.
The following default mods are provided by the mod compiler for common file manipulation.
Dangerous modifications rely on regular expressions (regex) to modify application code, which may cause the build to break. Regex mods are also difficult to version, and therefore should be used sparingly. Always opt towards using application code to modify application code, that is, Expo Modules native API.
Android mod | Dangerous | Description |
---|---|---|
mods.android.manifest | - | Modify the android/app/src/main/AndroidManifest.xml as JSON (parsed with xml2js ). |
mods.android.strings | - | Modify the android/app/src/main/res/values/strings.xml as JSON (parsed with xml2js ). |
mods.android.colors | - | Modify the android/app/src/main/res/values/colors.xml as JSON (parsed with xml2js ). |
mods.android.colorsNight | - | Modify the android/app/src/main/res/values-night/colors.xml as JSON (parsed with xml2js ). |
mods.android.styles | - | Modify the android/app/src/main/res/values/styles.xml as JSON (parsed with xml2js ). |
mods.android.gradleProperties | - | Modify the android/gradle.properties as a Properties.PropertiesItem[] . |
mods.android.mainActivity | Modify the android/app/src/main/<package>/MainActivity.java as a string. | |
mods.android.mainApplication | Modify the android/app/src/main/<package>/MainApplication.java as a string. | |
mods.android.appBuildGradle | Modify the android/app/build.gradle as a string. | |
mods.android.projectBuildGradle | Modify the android/build.gradle as a string. | |
mods.android.settingsGradle | Modify the android/settings.gradle as a string. |
iOS mod | Dangerous | Description |
---|---|---|
mods.ios.infoPlist | - | Modify the ios/<name>/Info.plist as JSON (parsed with @expo/plist ). |
mods.ios.entitlements | - | Modify the ios/<name>/<product-name>.entitlements as JSON (parsed with @expo/plist ). |
mods.ios.expoPlist | - | Modify the ios/<ame>/Expo.plist as JSON (Expo updates config for iOS) (parsed with @expo/plist ). |
mods.ios.xcodeproj | - | Modify the ios/<name>.xcodeproj as an XcodeProject object (parsed with xcode ). |
mods.ios.podfile | - | Modify the ios/Podfile as a string. |
mods.ios.podfileProperties | - | Modify the ios/Podfile.properties.json as JSON. |
mods.ios.appDelegate | Modify the ios/<name>/AppDelegate.m as a string. |
After the mods are resolved, the contents of each mod will be written to disk. Custom default mods can be added to support new native files.
For example, you can create a mod to support the GoogleServices-Info.plist
, and pass it to other mods.
Mods are responsible for a lot of tasks, so they can be pretty difficult to understand at first. If you're developing a feature that requires mods, it's best not to interact with them directly.
Instead you should use the helper mods provided by expo/config-plugins
:
Android mod | Mod plugin | Dangerous |
---|---|---|
mods.android.manifest | withAndroidManifest | - |
mods.android.strings | withStringsXml | - |
mods.android.colors | withAndroidColors | - |
mods.android.colorsNight | withAndroidColorsNight | - |
mods.android.styles | withAndroidStyles | - |
mods.android.gradleProperties | withGradleProperties | - |
mods.android.mainActivity | withMainActivity | |
mods.android.mainApplication | withMainApplication | |
mods.android.appBuildGradle | withAppBuildGradle | |
mods.android.projectBuildGradle | withProjectBuildGradle | |
mods.android.settingsGradle | withSettingsGradle |
iOS mod | Mod plugin | Dangerous |
---|---|---|
mods.ios.infoPlist | withInfoPlist | - |
mods.ios.entitlements | withEntitlementsPlist | - |
mods.ios.expoPlist | withExpoPlist | - |
mods.ios.xcodeproj | withXcodeProject | - |
mods.ios.podfile | withPodfile | - |
mods.ios.podfileProperties | withPodfileProperties | - |
mods.ios.appDelegate | withAppDelegate |
A mod plugin gets passed a config
object with additional properties modResults
and modRequest
added to it.
modResults
: The object to modify and return. The type depends on the mod that's being used.modRequest
: Additional properties supplied by the mod compiler.
projectRoot: string
: Project root directory for the universal app.platformProjectRoot: string
: Project root for the specific platform.modName: string
: Name of the mod.platform: ModPlatform
: Name of the platform used in the mods config.projectName?: string
: (iOS only) The path component used for querying project files. ex. projectRoot/ios/[projectName]/
Say you wanted to write a mod to update the Xcode Project's "product name":
import { ConfigPlugin, withXcodeProject, IOSConfig } from 'expo/config-plugins';
const withCustomProductName: ConfigPlugin<string> = (config, customName) => {
return withXcodeProject(
config,
async (
config
) => {
config.modResults = IOSConfig.Name.setProductName({ name: customName }, config.modResults);
return config;
}
);
};
// 💡 Usage:
/// Create a config
const config = {
name: 'my app',
};
/// Use the plugin
export default withCustomProductName(config, 'new_name');
Some parts of the mod system aren't fully fleshed out, these parts use withDangerousMod
to read/write data without a base mod.
These methods essentially act as their own base mod and cannot be extended.
Icons, for example, currently use the dangerous mod to perform a single generation step with no ability to customize the results.
export const withIcons = config => {
return withDangerousMod(config, [
'ios',
async config => {
await setIconsAsync(config, config.modRequest.projectRoot);
return config;
},
]);
};
Be careful using withDangerousMod
as it is subject to change in the future.
The order with which it gets executed is not reliable either.
Currently, dangerous mods run first before all other modifiers, this is because we use dangerous mods internally for large file system refactoring like when the package name changes.
The strings passed to the plugins
array can be resolved in a few different ways.
Any resolution pattern that isn't specified below is unexpected behavior, and subject to breaking changes.
You can quickly create a plugin in your project and use it in your config.
app.config.js
import "./my-config-plugin"
my-config-plugin.js
Imported from config
In this example, the config plugin file contains a bare minimum function:
module.exports = config => config;
Sometimes you want your package to export React components and also support a plugin. To do this, multiple entry points need to be used because the transpilation (Babel preset) may be different.
If an app.plugin.js file is present in the root of a Node module's folder, it'll be used instead of the package's main
file.
app.config.js
import "expo-splash-screen"
node_modules
expo-splash-screen
Node module
package.json
"main": "./build/index.js"
app.plugin.js
Entry file for custom plugins
build
index.js
Skipped in favor of app.plugin.js
module.exports = config => config;
A config plugin in a node module (without an app.plugin.js) will use the main
file defined in the package.json.
app.config.js
import "expo-splash-screen"
node_modules
expo-splash-screen
Node module
package.json
"main": "./build/index.js"
build
index.js
Node resolve to this file
This is different to how Config Plugins in Node modules work because app.plugin.js won't be resolved by default in a directory. You'll have to manually specify ./my-config-plugin/app.plugin.js
to use it, otherwise index.js in the directory will be used.
app.config.js
import "./my-config-plugin"
my-config-plugin
index.js
Config Plugin
app.plugin.js
Skipped outside of a node module
Avoid importing module internals.
If a file inside a Node module is directly imported, then the module's root app.plugin.js resolution will be skipped. This is referred to as "reaching inside a package" and is considered bad form. We support this to make testing, and plugin authoring easier, but we don't expect library authors to expose their plugins like this as a public API.
app.config.js
import "expo-splash-screen/build/index.js"
node_modules
expo-splash-screen
package.json
"main": "./build/index.js"
app.plugin.js
Ignored due to direct import
build
index.js
expo-splash-screen/build/index.js
Expo config objects also support passing functions as-is to the plugins
array. This is useful for testing, or if you want to use a plugin without creating a file.
const withCustom = (config, props) => config;
const config = {
plugins: [
[
withCustom,
{
/* props */
},
],
withCustom,
],
};
One caveat to using functions instead of strings is that serialization will replace the function with the function's name. This keeps manifests (kinda like the index.html for your app) working as expected.
Here is what the serialized config would look like:
{
"plugins": [["withCustom", {}], "withCustom"]
}
Config resolution searches for a file named app.plugin.js first when a Node module ID is provided as a plugin.
This is because Node environments are often different to Android, iOS, or web JS environments and therefore require different transpilation presets (for example, module.exports
instead of import/export
).
Because of this reasoning, the root of a Node module is searched instead of right next to the index.js. Imagine you had a TypeScript Node module where the transpiled main file was located at build/index.js, if app config plugin resolution searched for build/app.plugin.js you'd lose the ability to transpile the file differently.