Learn about mods and how to use them when creating a config plugin.


This guide explains what mods and mod plugins are, how they work, and how to use them effectively when creating config plugins for your Expo project.

Using the diagram below, in this guide, you will learn the last two parts of the config plugin hierarchy:

Mod plugins

Mod plugins provide a way to modify native project files during the prebuild process. They are made available from expo/config-plugins library and wrap top-level mods (also known as default mods) because top-level mods are platform-specific and perform various tasks that can be difficult to understand at first.

Tip: If you are developing a feature that requires mods, you should use mod plugins instead of interacting with top-level mods directly.

Available mod plugins

The following mod plugins are available in the expo/config-plugins library:

Android

Default Android modMod pluginDangerousDescription
mods.android.manifestwithAndroidManifest (Example)-Modify the android/app/src/main/AndroidManifest.xml as JSON (parsed with xml2js)
mods.android.stringswithStringsXml (Example)-Modify the android/app/src/main/res/values/strings.xml as JSON (parsed with xml2js).
mods.android.colorswithAndroidColors (Example)-Modify the android/app/src/main/res/values/colors.xml as JSON (parsed with xml2js).
mods.android.colorsNightwithAndroidColorsNight (Example)-Modify the android/app/src/main/res/values-night/colors.xml as JSON (parsed with xml2js).
mods.android.styleswithAndroidStyles (Example)-Modify the android/app/src/main/res/values/styles.xml as JSON (parsed with xml2js).
mods.android.gradlePropertieswithGradleProperties (Example)-Modify the android/gradle.properties as a Properties.PropertiesItem[].
mods.android.mainActivitywithMainActivity (Example)Modify the android/app/src/main/<package>/MainActivity.java as a string.
mods.android.mainApplicationwithMainApplication (Example)Modify the android/app/src/main/<package>/MainApplication.java as a string.
mods.android.appBuildGradlewithAppBuildGradle (Example)Modify the android/app/build.gradle as a string.
mods.android.projectBuildGradlewithProjectBuildGradle (Example)Modify the android/build.gradle as a string.
mods.android.settingsGradlewithSettingsGradle (Example)Modify the android/settings.gradle as a string.

iOS

Default iOS modMod pluginDangerousDescription
mods.ios.infoPlistwithInfoPlist (Example)-Modify the ios/<name>/Info.plist as JSON (parsed with @expo/plist).
mods.ios.entitlementswithEntitlementsPlist (Example)-Modify the ios/<name>/<product-name>.entitlements as JSON (parsed with @expo/plist).
mods.ios.expoPlistwithExpoPlist (Example)-Modify the ios/<name>/Expo.plist as JSON (Expo updates config for iOS) (parsed with @expo/plist).
mods.ios.xcodeprojwithXcodeProject (Example)-Modify the ios/<name>.xcodeproj as an XcodeProject object (parsed with xcode).
mods.ios.podfilewithPodfile (Example-Modify the ios/Podfile as a string.
mods.ios.podfilePropertieswithPodfileProperties (Example)-Modify the ios/Podfile.properties.json as JSON.
mods.ios.appDelegatewithAppDelegate (Example)Modify the ios/<name>/AppDelegate.m as a string.
Note about default Android and iOS mods:
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.

Mods

Config plugins use mods (short for modifiers) to modify native project files during the prebuild process. Mods are asynchronous functions that allow you to make changes to platform-specific files such as AndroidManifest.xml and Info.plist, and other native configuration files without having to manually edit them. They execute only during the syncing phase of npx expo prebuild (prebuild process).

They accept a config and a data object, then modify and return both of them as a single object. For example, in native projects, mods.android.manifest modifies AndroidManifest.xml and mods.ios.plist modifies Info.plist.

You don't use mods as top-level functions (for example with.android.manifest) directly in your config plugin. When you need to use a mod, you use mod plugins in your config plugins. These mod plugins are provided by the expo/config-plugins library and wrap top-level mod functions and behind the scenes they perform various tasks. To see a list of available mods, check out the mod plugins provided by expo/config-plugins.

How default mods work and their key characteristics

When a default mod resolves, it is added to the mods object of the app config. This mods object is different from the rest of the app config because it doesn't get serialized, which means you can use it to perform actions during code generation. Whenever possible, you should use available mod plugins instead of default mods since they are easier to work with.

Here is a high-level overview of how default mods work:

  • The config is read using getPrebuildConfig from @expo/prebuild-config
  • All of the core functionality supported by Expo is added via plugins in withIosExpoPlugins. This includes name, version, icons, locales, and so on.
  • The config is passed to the compiler compileModsAsync
  • The compiler adds base mods that are responsible for reading data (like Info.plist), executing a named mod (like mods.ios.infoPlist), then writing the results to the file system
  • The compiler iterates over all the mods and asynchronously evaluates them, providing some base props like the projectRoot
    • After each mod, error handling asserts if the mod chain was corrupted by an invalid mod

Here are some key characteristics of default mods:

  • 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:

    app.config.ts
    module.exports = {
      name: 'my-app',
      mods: {
        ios: {
          /* iOS mods... */
        },
        android: {
          /* Android mods... */
        },
      },
    };
    

After mods are resolved, the contents of each mod will be written to disk. Custom 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.

How mod plugins work

When a mod plugin is executed, it gets passed a config object with additional properties: modResults and modRequest.

modResults

The modResults object contains the data to modify and return. Its type depends on the mod that's being used.

modRequest

The modRequest object contains the following additional properties supplied by the mod compiler.

PropertyTypeDescription
projectRootstringProject root directory for the universal app.
platformProjectRootstringProject root for the specific platform.
modNamestringName of the mod.
platformModPlatformName of the platform used in the mods config.
projectNamestring(iOS only) The path component used for querying project files. For example, projectRoot/ios/[projectName]/.

Create your own mod

For example, if you want to write a mod to update the Xcode Project's "product name", you'll create a config plugin file that uses the withXcodeProject mod plugin.

my-config-plugin.ts
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');

Experimental functionality

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. For example, Icons currently use the dangerous mod to perform a single generation step with no ability to customize the results.

my-config-plugin.ts
import { ExpoConfig } from 'expo/config';

export const withIcons = (config: ExpoConfig): ExpoConfig => {
  return withDangerousMod(config, [
    'ios',
    async (config: ExpoConfig): Promise<ExpoConfig> => {
      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 because Expo uses dangerous mods internally for large file system refactoring like when a library's name changes.

Plugin module resolution

When implementing plugins, there are two fundamental approaches to consider:

  1. Plugins defined within your app's project: These plugins live locally within your project, making them easy to customize and maintain alongside your app's code. They are ideal for project-specific customizations.

  2. Standalone package plugins: These plugins exist as separate packages and are published to npm. This approach is ideal for reusable plugins that can be shared across multiple projects.

Both approaches provide the same capabilities for modifying your native configuration, but differ in how they're structured and imported. The sections below explain how module resolution works for each approach.

Any resolution pattern that isn't specified below is unexpected behavior, and subject to breaking changes.

Plugins defined within your app's project

With plugins defined within your app's project, you can implement plugins directly in your project in several ways:

File import

You can quickly create a plugin in your project by creating a JavaScript/TypeScript file and use it in your config like any other JS/TS file.

app.config.tsimport "./my-config-plugin"
my-config-plugin.ts Imported from config

In the above example, the config plugin file contains a bare minimum function:

my-config-plugin.ts
module.exports = ({ config }: { config: ExpoConfig }) => {};

Inline function inside of dynamic app config

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.

app.config.ts
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 (kind of like the index.html for your app) working as expected. Here is what the serialized config would look like:

{
  "plugins": [["withCustom", {}], "withCustom"]
}

Standalone package plugins

See Create a module with a config plugin for a step-by-step guide on how to create a standalone package plugin.

Standalone package plugins can be implemented in two ways:

1. Dedicated config plugin packages

These are npm packages whose sole purpose is to provide a config plugin. For a dedicated config plugin package, you can export your plugin using app.plugin.js:

app.config.tsimport "expo-splash-screen"
node_modules
expo-splash-screenNode module
  app.plugin.js Entry file for custom plugins
  build
   index.js Skipped in favor of app.plugin.js

2. Config plugins with companion packages

When a config plugin is part of a Node module without an app.plugin.js, it uses the package's main entry point:

app.config.tsimport "expo-splash-screen"
node_modules
expo-splash-screenNode module
  package.json"main": "./build/index.js"
  build
   index.js Node resolve to this file

Plugin resolution order

When you import a plugin package, files are resolved in this specific order:

  1. app.plugin.js in package root
app.config.tsimport "expo-splash-screen"
node_modules
expo-splash-screenNode 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
  1. Package's main entry (from package.json)
app.config.tsimport "expo-splash-screen"
node_modules
expo-splash-screenNode module
  package.json"main": "./build/index.js"
  build
   index.js Node resolve to this file
  1. Direct internal imports (not recommended)
Avoid importing module internals directly as it bypasses the standard resolution order and may break in future updates.
app.config.tsimport "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

Why use app.plugin.js for plugins

The app.plugin.js approach is preferred for config plugins as it allows different transpilation settings from the main package code. This is particularly important because Node environments often require different transpilation presets compared to Android, iOS, or web JS environments (for example, module.exports instead of import/export).