1---
2title: Developing and debugging a plugin
3description: Learn about development best practices and debugging techniques for Expo config plugins.
4sidebar_title: Development and debugging
5---
6
7import { Collapsible } from '~/ui/components/Collapsible';
8import { BoxLink } from '~/ui/components/BoxLink';
9
10Developing a plugin is a great way to extend the Expo ecosystem. However, there are times you'll want to debug your plugin. This page provides some of the best practices for developing and debugging a plugin.
11
12## Develop a Plugin
13
14> Use [modifier previews](https://github.com/expo/vscode-expo#expo-preview-modifier) to debug the results of your plugin live.
15
16To make plugin development easier, we've added plugin support to [`expo-module-scripts`](https://www.npmjs.com/package/expo-module-scripts).
17Refer to the [config plugins guide](https://github.com/expo/expo/tree/main/packages/expo-module-scripts#-config-plugin) for more info on using TypeScript, and Jest to build plugins.
18
19### Install dependencies
20
21Use the following dependencies in a library that provides a config plugin:
22
23```json package.json
24{
25  "dependencies": {},
26  "devDependencies": {
27    "expo": "^47.0.0"
28  },
29  "peerDependencies": {
30    "expo": ">=47.0.0"
31  },
32  "peerDependenciesMeta": {
33    "expo": {
34      "optional": true
35    }
36  }
37}
38```
39
40- You may update the exact version of `expo` to build against a specific version.
41- For simple config plugins that depend on core, stable APIs, such as a plugin that only modifies **AndroidManifest.xml** or **Info.plist**, you can use a loose dependency such as in the example above.
42- You may also want to install [`expo-module-scripts`](https://github.com/expo/expo/blob/main/packages/expo-module-scripts/README.md) as a development dependency, but it's not required.
43
44### Import the config plugins package
45
46The `expo/config-plugins` and `expo/config` packages are re-exported from the `expo` package.
47
48{/* prettier-ignore */}
49```js
50const { /* @hide ...*//* @end */ } = require('expo/config-plugins');
51const { /* @hide ...*//* @end */ } = require('expo/config');
52```
53
54Importing through the `expo` package ensures that you are using the version of the `expo/config-plugins` and `expo/config` packages that are depended on by the `expo` package.
55
56If you do not import the package through the `expo` re-export in this way, you may accidentally be importing an incompatible version
57(depending on the implementation details of module hoisting in the package manager used by the developer consuming the module) or be unable to import the module at all
58(if using "plug and play" features of a package manager such as Yarn Berry or pnpm).
59
60Config types are exported directly from `expo/config`, so there is no need to install or import from `expo/config-types`:
61
62```ts
63import { ExpoConfig, ConfigContext } from 'expo/config';
64```
65
66<Collapsible summary="Using SDK 46 or lower?">
67
68For SDK 46 and lower, import the `@expo/config-plugins` package directly. This is installed automatically by the `expo` package, but not re-exported as it is in SDK 47 and higher.
69
70{/* prettier-ignore */}
71```js
72const { /* @hide ...*//* @end */ } = require('@expo/config-plugins');
73```
74
75</Collapsible>
76
77### Best practices for mods
78
79- Avoid regex: [static modification](#static-modification) is key. If you want to modify a value in an Android gradle file, consider using `gradle.properties`. If you want to modify some code in the Podfile, consider writing to JSON and having the Podfile read the static values.
80- Avoid performing long-running tasks like making network requests or installing Node modules in mods.
81- Do not add interactive terminal prompts in mods.
82- Generate, move, and delete new files in dangerous mods only. Failing to do so will break [introspection](#introspection).
83- Utilize built-in config plugins like `withXcodeProject` to minimize the amount of times a file is read and parsed.
84- Stick with the XML parsing libraries that prebuild uses internally, this helps prevent changes where code is rearranged needlessly.
85
86### Tooling
87
88We highly recommend installing the [Expo Tools VS Code plugin](https://marketplace.visualstudio.com/items?itemName=byCedric.vscode-expo)
89as this will perform automatic validation on the plugins and surface error information along with other quality of life improvements for Config Plugin development.
90
91### Setup up a playground environment
92
93You can develop plugins easily using JS, but if you want to setup Jest tests and use TypeScript, you will want a monorepo.
94
95A monorepo will enable you to work on a node module and import it in your Expo config like you would if it were published to npm.
96Expo config plugins have full monorepo support built-in so all you need to do is setup a project.
97
98In your monorepo's `packages/` folder, create a module, and [bootstrap a config plugin](https://github.com/expo/expo/tree/main/packages/expo-module-scripts#-config-plugin) in it.
99
100### Manually run a plugin
101
102If you aren't comfortable with setting up a monorepo, you can try manually running a plugin:
103
104- Run `npm pack` in the package with the config plugin
105- In your test project, run `npm install path/to/react-native-my-package-1.0.0.tgz`, this will add the package to your **package.json** `dependencies` object.
106- Add the package to the `plugins` array in your **app.json**: `{ "plugins": ["react-native-my-package"] }`
107  - If you have [VS Code Expo Tools][vscode-expo] installed, autocomplete should work for the plugin.
108- If you need to update the package, change the `version` in the package's **package.json** and repeat the process.
109
110### Modify AndroidManifest.xml
111
112Packages should attempt to use the built-in **AndroidManifest.xml** [merging system](https://developer.android.com/studio/build/manage-manifests)
113before using a config plugin. This can be used for static, non-optional features like permissions.
114This will ensure features are merged during build-time and not prebuild-time, which minimizes the possibility of users forgetting to prebuild.
115The drawback is that users cannot use [introspection](#introspection) to preview the changes and debug any potential issues.
116
117Here is an example of a package's **AndroidManifest.xml**, which injects a required permission:
118
119```xml AndroidManifest.xml
120<!-- @info Include <code>xmlns:android="..."</code> to use <code>android:*</code> properties like <code>android:name</code> in your manifest. -->
121<manifest package="expo.modules.filesystem" xmlns:android="http://schemas.android.com/apk/res/android">
122  <!-- @end -->
123  <uses-permission android:name="android.permission.INTERNET"/>
124</manifest>
125```
126
127If you're building a plugin for your local project, or if your package needs more control, then you should implement a plugin.
128
129You can use built-in types and helpers to ease the process of working with complex objects.
130Here's an example of adding a `<meta-data android:name="..." android:value="..."/>` to the default `<application android:name=".MainApplication" />`.
131
132```ts my-config-plugin.ts
133import { AndroidConfig, ConfigPlugin, withAndroidManifest } from 'expo/config-plugins';
134import { ExpoConfig } from 'expo/config';
135
136// Use these imports in SDK 46 and lower
137// import { AndroidConfig, ConfigPlugin, withAndroidManifest } from '@expo/config-plugins';
138// import { ExpoConfig } from '@expo/config-types';
139
140// Using helpers keeps error messages unified and helps cut down on XML format changes.
141const { addMetaDataItemToMainApplication, getMainApplicationOrThrow } = AndroidConfig.Manifest;
142
143export const withMyCustomConfig: ConfigPlugin = config => {
144  return withAndroidManifest(config, async config => {
145    // Modifiers can be async, but try to keep them fast.
146    config.modResults = await setCustomConfigAsync(config, config.modResults);
147    return config;
148  });
149};
150
151// Splitting this function out of the mod makes it easier to test.
152async function setCustomConfigAsync(
153  config: Pick<ExpoConfig, 'android'>,
154  androidManifest: AndroidConfig.Manifest.AndroidManifest
155): Promise<AndroidConfig.Manifest.AndroidManifest> {
156  const appId = 'my-app-id';
157  // Get the <application /> tag and assert if it doesn't exist.
158  const mainApplication = getMainApplicationOrThrow(androidManifest);
159
160  addMetaDataItemToMainApplication(
161    mainApplication,
162    // value for `android:name`
163    'my-app-id-key',
164    // value for `android:value`
165    appId
166  );
167
168  return androidManifest;
169}
170```
171
172### Modify Info.plist
173
174Using the `withInfoPlist` is a bit safer than statically modifying the `expo.ios.infoPlist` object in the **app.json** because it reads the contents of the Info.plist and merges it with the `expo.ios.infoPlist`, this means you can attempt to keep your changes from being overwritten.
175
176Here's an example of adding a `GADApplicationIdentifier` to the **Info.plist**:
177
178```ts my-config-plugin.ts
179import { ConfigPlugin, withInfoPlist } from 'expo/config-plugins';
180
181// Use these imports in SDK 46 and lower
182// import { ConfigPlugin, InfoPlist, withInfoPlist } from '@expo/config-plugins';
183// import { ExpoConfig } from '@expo/config-types';
184
185// Pass `<string>` to specify that this plugin requires a string property.
186export const withCustomConfig: ConfigPlugin<string> = (config, id) => {
187  return withInfoPlist(config, config => {
188    config.modResults.GADApplicationIdentifier = id;
189    return config;
190  });
191};
192```
193
194### Modify iOS Podfile
195
196The iOS **Podfile** is the config file for CocoaPods, the dependency manager on iOS. It is similar to **package.json** for iOS.
197The **Podfile** is a ruby file (application code), which means you **cannot** safely modify it from Expo config plugins and should opt for another approach, such as [Expo Autolinking](modules/autolinking) hooks.
198
199Currently, we do have a configuration that interacts with the CocoaPods file though.
200
201Podfile configuration is often done with environment variables:
202
203- `process.env.EXPO_USE_SOURCE` when set to `1`, Expo modules will install source code instead of xcframeworks.
204- `process.env.CI` in some projects, when set to `0`, Flipper installation will be skipped.
205
206We do expose one mechanism for safely interacting with the Podfile, but it's very limited.
207The versioned [template Podfile](https://github.com/expo/expo/tree/main/templates/expo-template-bare-minimum/ios/Podfile) is hard coded to read
208from a static JSON file **Podfile.properties.json**, we expose a mod (`ios.podfileProperties`, `withPodfileProperties`) to safely read and write from this file.
209This is used by [expo-build-properties](/versions/latest/sdk/build-properties) and to configure the JavaScript engine.
210
211### Add plugins to `pluginHistory`
212
213`_internal.pluginHistory` was created to prevent duplicate plugins from running while migrating from legacy UNVERSIONED plugins to versioned plugins.
214
215```ts my-config-plugin.ts
216import { ConfigPlugin, createRunOncePlugin } from 'expo/config-plugins';
217
218// Use this import in SDK 46 and lower
219// import { ConfigPlugin, createRunOncePlugin } from '@expo/config-plugins';
220
221// Keeping the name, and version in sync with it's package.
222const pkg = require('my-cool-plugin/package.json');
223
224const withMyCoolPlugin: ConfigPlugin = config => config;
225
226// A helper method that wraps `withRunOnce` and appends items to `pluginHistory`.
227export default createRunOncePlugin(
228  // The plugin to guard.
229  withMyCoolPlugin,
230  // An identifier used to track if the plugin has already been run.
231  pkg.name,
232  // Optional version property, if omitted, defaults to UNVERSIONED.
233  pkg.version
234);
235```
236
237### Plugin development best practices
238
239- **Instructions in your README**: If the plugin is tied to a React Native module, then you should document manual setup instructions for the package.
240  If anything goes wrong with the plugin, users should still be able to manually add the package to their project.
241  Doing this often helps you to find ways to reduce the setup, which can lead to a simpler plugin.
242  - Document the available properties for the plugin, and specify if the plugin works without props.
243  - If you can make your plugin work after running prebuild multiple times, that’s a big plus! It can improve the developer experience to be able to run `expo prebuild` without the `--clean` flag to sync changes.
244- **Naming conventions**: Use `withFeatureName` if cross-platform. If the plugin is platform specific, use a camel case naming with the platform right after “with”. For example, `withAndroidSplash`, `withIosSplash`.
245  There is no universally agreed upon casing for `iOS` in camel cased identifiers, we prefer this style and suggest using it for your config plugins too.
246- **Leverage built-in plugins**: Account for built-in plugins from the [prebuild config](https://github.com/expo/expo-cli/blob/master/packages/prebuild-config/src/plugins/withDefaultPlugins.ts).
247  Some features are included for historical reasons, like the ability to automatically copy and link [Google services files](https://github.com/expo/expo-cli/blob/3a0ef962a27525a0fe4b7e5567fb7b3fb18ec786/packages/config-plugins/src/ios/Google.ts#L15) defined in the Expo config.
248  If there is overlap, then maybe recommend the user uses the built-in types to keep your plugin as simple as possible.
249- **Split up plugins by platform**: For example — `withIosSplash`, `withAndroidSplash`. This makes using the `--platform` flag in `expo prebuild` a bit easier to follow in `EXPO_DEBUG` mode.
250- **Unit test your plugin**: Write Jest tests for complex modifications. If your plugin requires access to the filesystem,
251  use a mock system (we strongly recommend [`memfs`][memfs]), you can see examples of this in the [`expo-notifications`](https://github.com/expo/expo/blob/fc3fb2e81ad3a62332fa1ba6956c1df1c3186464/packages/expo-notifications/plugin/src/__tests__/withNotificationsAndroid-test.ts#L34) plugin tests.
252  - Notice the root [\*\*/\_\_mocks\_\_/\*\*/\*](https://github.com/expo/expo/tree/main/packages/expo-notifications/plugin/__mocks__) folder and [**plugin/jest.config.js**](https://github.com/expo/expo/tree/main/packages/expo-notifications/plugin/jest.config.js).
253- A TypeScript plugin is always better than a JavaScript plugin. Check out the [`expo-module-script` plugin][ems-plugin] tooling for more info.
254- Do not modify the `sdkVersion` via a config plugin, this can break commands like `expo install` and cause other unexpected issues.
255
256### Versioning
257
258By default, `expo prebuild` runs transformations on a [source template][source-template] associated with the Expo SDK version that a project is using.
259The SDK version is defined in the **app.json** or inferred from the installed version of `expo` that the project has.
260
261When Expo SDK upgrades to a new version of React Native for instance, the template may change significantly to account for changes in React Native or new releases of Android or iOS.
262
263If your plugin is mostly using [static modifications](#static-modification) then it will work well across versions.
264If it's using a regular expression to transform application code, then you'll definitely want to document which Expo SDK version your plugin is intended for.
265Expo releases a new version quarterly (every 3 months), and there is a [beta period](https://github.com/expo/expo/blob/main/guides/releasing/Release%20Workflow.md#stage-5---beta-release) where you can test if your plugin works with the new version before it's released.
266
267{/* TODO: versioned plugin wrapper */}
268
269### Plugin properties
270
271Properties are used to customize the way a plugin works during prebuild.
272
273Properties MUST always be static values (no functions, or promises). Consider the following types:
274
275```ts
276type StaticValue = boolean | number | string | null | StaticArray | StaticObject;
277
278type StaticArray = StaticValue[];
279
280interface StaticObject {
281  [key: string]: StaticValue | undefined;
282}
283```
284
285Static properties are required because the Expo config must be serializable to JSON for use as the app manifest.
286Static properties can also enable tooling that generates JSON schema type checking for autocomplete and IntelliSense.
287
288If possible, attempt to make your plugin work without props, this will help resolution tooling like [`expo install`](#expo-install) or [VS Code Expo Tools][vscode-expo] work better.
289Remember that every property you add increases complexity, making it harder to change in the future and increase the amount of features you'll need to test.
290Good default values are preferred over mandatory configuration when feasible.
291
292### Configure Android app startup
293
294You may find that your project requires configuration to be setup before the JS engine has started.
295For example, in `expo-splash-screen` on Android, we need to specify the resize mode in the **MainActivity.java**'s `onCreate` method.
296Instead of attempting to dangerously regex these changes into the `MainActivity` via a dangerous mod, we use a system of lifecycle hooks and static settings
297to safely ensure the feature works across all supported Android languages (Java, Kotlin), versions of Expo, and combination of config plugins.
298
299This system is made up of three components:
300
301- `ReactActivityLifecycleListeners`: An interface exposed by `expo-modules-core` to get a native callback when the project `ReactActivity`'s `onCreate` method is invoked.
302- `withStringsXml`: A mod exposed by `expo/config-plugins` which writes a property to the Android **strings.xml** file, the library can safely read the strings.xml value and do initial setup. The string XML values follow a designated format for consistency.
303- `SingletonModule` (optional): An interface exposed by `expo-modules-core` to create a shared interface between native modules and `ReactActivityLifecycleListeners`.
304
305Consider this example: We want to set a custom "value" string to a property on the Android `Activity`, directly after the `onCreate` method was invoked.
306We can do this safely by creating a node module `expo-custom`, implementing `expo-modules-core`, and Expo config plugins:
307
308First, we register the `ReactActivity` listener in our Android native module, this will only be invoked if the user has `expo-modules-core` support, setup in their project (default in projects bootstrapped with Expo CLI, Create React Native App, Ignite CLI, and Expo prebuilding).
309
310```kotlin expo-custom/android/src/main/java/expo/modules/custom/CustomPackage.kt
311package expo.modules.custom
312
313import android.content.Context
314import expo.modules.core.BasePackage
315import expo.modules.core.interfaces.ReactActivityLifecycleListener
316
317class CustomPackage : BasePackage() {
318  override fun createReactActivityLifecycleListeners(activityContext: Context): List<ReactActivityLifecycleListener> {
319    return listOf(CustomReactActivityLifecycleListener(activityContext))
320  }
321
322  // ...
323}
324```
325
326Next we implement the `ReactActivity` listener, this is passed the `Context` and is capable of reading from the project **strings.xml** file.
327
328```kotlin expo-custom/android/src/main/java/expo/modules/custom/CustomReactActivityLifecycleListener.kt
329package expo.modules.custom
330
331import android.app.Activity
332import android.content.Context
333import android.os.Bundle
334import android.util.Log
335import expo.modules.core.interfaces.ReactActivityLifecycleListener
336
337class CustomReactActivityLifecycleListener(activityContext: Context) : ReactActivityLifecycleListener {
338  override fun onCreate(activity: Activity, savedInstanceState: Bundle?) {
339    // Execute static tasks before the JS engine starts.
340    // These values are defined via config plugins.
341
342    var value = getValue(activity)
343    if (value != "") {
344      // Do something to the Activity that requires the static value...
345    }
346  }
347
348  // Naming is node module name (`expo-custom`) plus value name (`value`) using underscores as a delimiter
349  // i.e. `expo_custom_value`
350  // `@expo/vector-icons` + `iconName` -> `expo__vector_icons_icon_name`
351  private fun getValue(context: Context): String = context.getString(R.string.expo_custom_value).toLowerCase()
352}
353```
354
355We must define default **string.xml** values which the user will overwrite locally by using the same `name` property in their **strings.xml** file.
356
357```xml expo-custom/android/src/main/res/values/strings.xml
358<?xml version="1.0" encoding="utf-8"?>
359<resources>
360    <string name="expo_custom_value" translatable="false"></string>
361</resources>
362```
363
364At this point, bare users can configure this value by creating a string in their local **strings.xml** file (assuming they also have `expo-modules-core` support setup):
365
366```xml ./android/app/src/main/res/values/strings.xml
367<?xml version="1.0" encoding="utf-8"?>
368<resources>
369    <string name="expo_custom_value" translatable="false">I Love Expo</string>
370</resources>
371```
372
373For managed users, we can expose this functionality (safely!) via an Expo config plugin:
374
375```js expo-custom/app.plugin.js
376const { AndroidConfig, withStringsXml } = require('expo/config-plugins');
377
378function withCustom(config, value) {
379  return withStringsXml(config, config => {
380    config.modResults = setStrings(config.modResults, value);
381    return config;
382  });
383}
384
385function setStrings(strings, value) {
386  // Helper to add string.xml JSON items or overwrite existing items with the same name.
387  return AndroidConfig.Strings.setStringItem(
388    [
389      // XML represented as JSON
390      // <string name="expo_custom_value" translatable="false">value</string>
391      { $: { name: 'expo_custom_value', translatable: 'false' }, _: value },
392    ],
393    strings
394  );
395}
396```
397
398Managed Expo users can now interact with this API like so:
399
400```json app.json
401{
402  "expo": {
403    "plugins": [["expo-custom", "I Love Expo"]]
404  }
405}
406```
407
408By re-running `expo prebuild -p` (`eas build -p android`, or `expo run:ios`) the user can now see the changes, safely applied in their managed project!
409
410As you can see from the example, we rely heavily on application code (expo-modules-core) to interact with application code (the native project). This ensures that our config plugins are safe and reliable, hopefully for a very long time!
411
412## Debug config plugins
413
414You can debug config plugins by running `EXPO_DEBUG=1 expo prebuild`. If `EXPO_DEBUG` is enabled, the plugin stack logs will be printed, these are useful for viewing which mods ran, and in what order they ran in. To view all static plugin resolution errors, enable `EXPO_CONFIG_PLUGIN_VERBOSE_ERRORS`, this should only be needed for plugin authors. By default, some automatic plugin errors are hidden because they're usually related to versioning issues and aren't very helpful (that is, legacy package doesn't have a config plugin yet).
415
416Running `expo prebuild --clean` with remove the generated native folders before compiling.
417
418You can also run `expo config --type prebuild` to print the results of the plugins with the mods unevaluated (no code is generated).
419
420Expo CLI commands can be profiled using `EXPO_PROFILE=1`.
421
422## Introspection
423
424Introspection is an advanced technique used to read the evaluated results of modifiers without generating any code in the project.
425This can be used to quickly debug the results of [static modifications](#static-modification) without needing to run prebuild.
426You can interact with introspection live, by using the [preview feature](https://github.com/expo/vscode-expo#expo-preview-modifier) of `vscode-expo`.
427
428You can try introspection by running `expo config --type introspect` in a project.
429
430Introspection only supports a subset of modifiers:
431
432- `android.manifest`
433- `android.gradleProperties`
434- `android.strings`
435- `android.colors`
436- `android.colorsNight`
437- `android.styles`
438- `ios.infoPlist`
439- `ios.entitlements`
440- `ios.expoPlist`
441- `ios.podfileProperties`
442
443> Introspection only works on safe modifiers (static files like JSON, XML, plist, properties), with the exception of `ios.xcodeproj` which often requires file system changes, making it non idempotent.
444
445Introspection works by creating custom base mods that work like the default base mods, except they don't write the `modResults` to disk at the end.
446Instead of persisting, they save the results to the Expo config under `_internal.modResults`, followed by the name of the mod
447such as the `ios.infoPlist` mod saves to `_internal.modResults.ios.infoPlist: {}`.
448
449As a real-world example, introspection is used by `eas-cli` to determine what the final iOS entitlements will be in a managed app,
450so it can sync them with the Apple Developer Portal before building. Introspection can also be used as a handy debugging and development tool.
451
452{/* TODO: Link to VS Code extension after preview feature lands */}
453
454## Legacy plugins
455
456To make `eas build` work the same as the classic `expo build` service, we added support for "legacy plugins" which are applied automatically to a project when they're installed in the project.
457
458For instance, say a project has `expo-camera` installed but doesn't have `plugins: ['expo-camera']` in their **app.json**.
459Expo CLI would automatically add `expo-camera` to the plugins to ensure that the required camera and microphone permissions are added to the project.
460The user can still customize the `expo-camera` plugin by adding it to the `plugins` array manually, and the manually defined plugins will take precedence over the automatic plugins.
461
462You can debug which plugins were added by running `expo config --type prebuild` and seeing the `_internal.pluginHistory` property.
463
464This will show an object with all plugins that were added using `withRunOnce` plugin from `expo/config-plugins`.
465
466Notice that `expo-location` uses `version: '11.0.0'`, and `react-native-maps` uses `version: 'UNVERSIONED'`. This means the following:
467
468- `expo-location` and `react-native-maps` are both installed in the project.
469- `expo-location` is using the plugin from the project's `node_modules/expo-location/app.plugin.js`
470- The version of `react-native-maps` installed in the project doesn't have a plugin, so it's falling back on the unversioned plugin that is shipped with `expo-cli` for legacy support.
471
472```json
473{
474  _internal: {
475    pluginHistory: {
476      'expo-location': {
477        name: 'expo-location',
478        version: '11.0.0',
479      },
480      'react-native-maps': {
481        name: 'react-native-maps',
482        version: 'UNVERSIONED',
483      },
484    },
485  },
486};
487```
488
489For the most _stable_ experience, you should try to have no `UNVERSIONED` plugins in your project. This is because the `UNVERSIONED` plugin may not support the native code in your project.
490For instance, say you have an `UNVERSIONED` Facebook plugin in your project, if the Facebook native code or plugin has a breaking change, that will break the way your project prebuilds and cause it to error on build.
491
492## Static Modification
493
494Plugins can transform application code with regular expressions, but these modifications are dangerous, if the template changes over time
495then the regex becomes hard to predict (similarly, if the user modifies a file manually or uses a custom template).
496Here are some examples of files you shouldn't modify manually, and alternatives.
497
498### Android Gradle Files
499
500Gradle files are written in either Groovy or Kotlin. They are used to manage dependencies, versioning, and other settings in the Android app.
501Instead of modifying them directly with the `withProjectBuildGradle`, `withAppBuildGradle`, or `withSettingsGradle` mods, utilize the static `gradle.properties` file.
502
503The `gradle.properties` is a static key/value pair that groovy files can read from. For example, say you wanted to control some toggle in Groovy:
504
505```properties gradle.properties
506# @info Safely modified using the <code>withGradleProperties()</code> mod. #
507expo.react.jsEngine=hermes
508# @end #
509```
510
511Then later in a Gradle file:
512
513```groovy app/build.gradle
514project.ext.react = [/* @info This code would be added to the template ahead of time, but it could be regexed in using <code>withAppBuildGradle()</code> */ enableHermes: findProperty('expo.react.jsEngine') ?: 'jsc' /* @end */]
515```
516
517- For keys in the `gradle.properties`, use camel case separated by `.`s, and usually starting with the `expo` prefix to denote that the property is managed by prebuild.
518- To access the property, use one of two global methods:
519  - `property`: Get a property, throw an error if the property is not defined.
520  - `findProperty`: Get a property without throwing an error if the property is missing. This can often be used with the `?:` operator to provide a default value.
521
522Generally, you should only interact with the Gradle file via Expo [Autolinking][autolinking], this provides a programmatic interface with the project files.
523
524### iOS App Delegate
525
526Some modules may need to add delegate methods to the project AppDelegate, this can be done dangerously via the `withAppDelegate` mod,
527or it can be done safely by adding support for unimodules AppDelegate proxy to the native module.
528The unimodules AppDelegate proxy can swizzle function calls to native modules in a safe and reliable way.
529If the language of the project AppDelegate changes from Objective-C to Swift, the swizzler will continue to work, whereas a regex would possibly fail.
530
531Here are some examples of the AppDelegate proxy in action:
532
533- `expo-app-auth` -- [**EXAppAuthAppDelegate.m**](https://github.com/expo/expo/blob/bd7bc03ee10d89487eac25351a455bd9db155b8c/packages/expo-app-auth/ios/EXAppAuth/EXAppAuthAppDelegate.m) (openURL)
534- `expo-branch` -- [**EXBranchManager.m**](https://github.com/expo/expo/blob/636b55ab767f502f29c922a34821434efff04034/packages/expo-branch/ios/EXBranch/EXBranchManager.m) (didFinishLaunchingWithOptions, continueUserActivity, openURL)
535- `expo-notifications` -- [**EXPushTokenManager.m**](https://github.com/expo/expo/blob/bd469e421856f348d539b1b57325890147935dbc/packages/expo-notifications/ios/EXNotifications/PushToken/EXPushTokenManager.m) (didRegisterForRemoteNotificationsWithDeviceToken, didFailToRegisterForRemoteNotificationsWithError)
536- `expo-facebook` -- [**EXFacebookAppDelegate.m**](https://github.com/expo/expo/blob/e0bb254c889734f2ec6c7b688167f013587ed201/packages/expo-facebook/ios/EXFacebook/EXFacebookAppDelegate.m) (openURL)
537- `expo-file-system` -- [**EXSessionHandler.m**](https://github.com/expo/expo/blob/e0bb254c889734f2ec6c7b688167f013587ed201/packages/expo-file-system/ios/EXFileSystem/EXSessionTasks/EXSessionHandler.m) (handleEventsForBackgroundURLSession)
538
539Currently, the only known way to add support for the AppDelegate proxy to a native module, without converting that module to a unimodule,
540is to create a wrapper package: [example](https://github.com/expo/expo/pull/5165).
541
542We plan to improve this in the future.
543
544### iOS CocoaPods Podfile
545
546The `ios/Podfile` can be customized dangerously with regex, or statically via JSON:
547
548```ruby Podfile
549require 'json'
550
551# @info Import a JSON file and parse it in Ruby #
552podfileConfig = JSON.parse(File.read(File.join(__dir__, 'podfile.config.json')))
553# @end #
554
555platform :ios, '11.0'
556
557target 'yolo27' do
558  use_unimodules!
559  config = use_native_modules!
560  use_react_native!(:path => config["reactNativePath"])
561
562  # podfileConfig['version']
563end
564```
565
566Generally, you should only interact with the Podfile via Expo [Autolinking][autolinking], this provides a programmatic interface with the project files.
567
568### Custom Base Modifiers
569
570The Expo CLI `expo prebuild` command uses [`@expo/prebuild-config`][prebuild-config] to get the default base modifiers. These defaults only manage a subset of common files, if you want to manage custom files you can do that locally by adding new base modifiers.
571
572For example, say you wanted to add support for managing the `ios/*/AppDelegate.h` file, you could do this by adding a `ios.appDelegateHeader` modifier.
573
574> This example uses `ts-node` for simple local TypeScript support, this isn't strictly necessary. [Learn more](/guides/typescript/#appconfigjs).
575
576```ts withAppDelegateHeaderBaseMod.ts
577import { ConfigPlugin, IOSConfig, Mod, withMod, BaseMods } from 'expo/config-plugins';
578import fs from 'fs';
579
580/**
581 * A plugin which adds new base modifiers to the prebuild config.
582 */
583export function withAppDelegateHeaderBaseMod(config) {
584  return BaseMods.withGeneratedBaseMods<'appDelegateHeader'>(config, {
585    platform: 'ios',
586    providers: {
587      // Append a custom rule to supply AppDelegate header data to mods on `mods.ios.appDelegateHeader`
588      appDelegateHeader: BaseMods.provider<IOSConfig.Paths.AppDelegateProjectFile>({
589        // Get the local filepath that should be passed to the `read` method.
590        getFilePath({ modRequest: { projectRoot } }) {
591          const filePath = IOSConfig.Paths.getAppDelegateFilePath(projectRoot);
592          // Replace the .m with a .h
593          if (filePath.endsWith('.m')) {
594            return filePath.substr(0, filePath.lastIndexOf('.')) + '.h';
595          }
596          // Possibly a Swift project...
597          throw new Error(`Could not locate a valid AppDelegate.h at root: "${projectRoot}"`);
598        },
599        // Read the input file from the filesystem.
600        async read(filePath) {
601          return IOSConfig.Paths.getFileInfo(filePath);
602        },
603        // Write the resulting output to the filesystem.
604        async write(filePath: string, { modResults: { contents } }) {
605          await fs.promises.writeFile(filePath, contents);
606        },
607      }),
608    },
609  });
610}
611
612/**
613 * (Utility) Provides the AppDelegate header file for modification.
614 */
615export const withAppDelegateHeader: ConfigPlugin<Mod<IOSConfig.Paths.AppDelegateProjectFile>> = (
616  config,
617  action
618) => {
619  return withMod(config, {
620    platform: 'ios',
621    mod: 'appDelegateHeader',
622    action,
623  });
624};
625
626// (Example) Log the contents of the modifier.
627export const withSimpleAppDelegateHeaderMod = config => {
628  return withAppDelegateHeader(config, config => {
629    console.log('modify header:', config.modResults);
630    return config;
631  });
632};
633```
634
635To use this new base mod, add it to the plugins array. The base mod **MUST** be added last after all other plugins that use the mod, this is because it must write the results to disk at the end of the process.
636
637```js app.config.js
638// Required for external files using TS
639require('ts-node/register');
640
641import {
642  withAppDelegateHeaderBaseMod,
643  withSimpleAppDelegateHeaderMod,
644} from './withAppDelegateHeaderBaseMod.ts';
645
646export default ({ config }) => {
647  if (!config.plugins) config.plugins = [];
648  config.plugins.push(
649    withSimpleAppDelegateHeaderMod,
650
651    // Base mods MUST be last
652    withAppDelegateHeaderBaseMod
653  );
654  return config;
655};
656```
657
658For more info, see [the PR that adds support](https://github.com/expo/expo-cli/pull/3852) for this feature.
659
660## expo install
661
662Node modules with config plugins can be added to the project's Expo config automatically by using the `expo install` command. [Related PR](https://github.com/expo/expo-cli/pull/3437).
663
664This makes setup a bit easier and helps prevent users from forgetting to add a plugin.
665
666This does come with a couple of caveats:
667
6681. Packages must export a plugin via **app.plugin.js**, this rule was added to prevent popular packages like `lodash` from being mistaken for a config plugin and breaking the prebuild.
6692. There is currently no mechanism for detecting if a config plugin has mandatory props. Because of this, `expo install` will only add the plugin, and not attempt to add any extra props. For example, `expo-camera` has optional extra props, so `plugins: ['expo-camera']` is valid, but if it had mandatory props then `expo-camera` would throw an error.
6703. Plugins can only be automatically added when the user's project uses a static Expo config (**app.json** and **app.config.json**).
671   If the user runs `expo install expo-camera` in a project with an **app.config.js**, they'll see a warning like:
672
673```
674Cannot automatically write to dynamic config at: app.config.js
675Please add the following to your Expo config
676
677{
678  "plugins": [
679    "expo-camera"
680  ]
681}
682```
683
684[config-docs]: /versions/latest/config/app/
685[prebuild-config]: https://github.com/expo/expo-cli/tree/main/packages/prebuild-config#readme
686[cli-prebuild]: /workflow/expo-cli/#expo-prebuild
687[configplugin]: https://github.com/expo/expo-cli/blob/3a0ef962a27525a0fe4b7e5567fb7b3fb18ec786/packages/config-plugins/src/Plugin.types.ts#L76
688[source-template]: https://github.com/expo/expo/tree/main/templates/expo-template-bare-minimum
689[expo-beta-docs]: https://github.com/expo/expo/tree/main/guides/releasing/Release%20Workflow.mdx#stage-5---beta-release
690[vscode-expo]: https://marketplace.visualstudio.com/items?itemName=byCedric.vscode-expo
691[ems-plugin]: https://github.com/expo/expo/tree/main/packages/expo-module-scripts#-config-plugin
692[xml2js]: https://www.npmjs.com/package/xml2js
693[expo-plist]: https://www.npmjs.com/package/@expo/plist
694[memfs]: https://www.npmjs.com/package/memfs
695[emc]: https://github.com/expo/expo/tree/main/packages/expo-modules-core
696[autolinking]: /more/glossary-of-terms#autolinking
697