new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

Expo & React Native · all subjects

config-plugins

207 notes in this subject, read out of this brain and free to use. This is page 1 of 4.

Why dangerous mods are considered dangerous

Automated direct source code manipulation does not compose well. If one dangerous mod replaces text and a subsequent dangerous mod expects the original text (perhaps as a regex anchor), the result may be unpredictable, throwing an error or logging incorrectly. Unlike standard mods, dangerous mods are rarely guaranteed to be idempotent—running the same dangerous mod multiple times may produce different results, cause duplicate modifications, or break the target file entirely.

Available path properties in config plugins

Path properties available in config plugins: | Path | Type | Description | | --- | --- | --- | | config.modRequest.projectRoot | string | Universal app project root directory where package.json is located. Used for resolving assets, reading package.json, and cross-platform operations. Always verify the directory exists and contains package.json. | | config.modRequest.platformProjectRoot | string | Platform-specific project root (projectRoot/android or projectRoot/ios). Used for platform-specific file operations like modifying native configuration files. Ensure the platform directory exists relative to main projectRoot. | | config.modRequest.projectName | string | [iOS only] Project name component for constructing iOS file paths (for example, projectRoot/ios/[projectName]/). Used for iOS-specific file path construction. Only available on iOS platform and should match the actual Xcode project structure. | | config.modRequest.introspect | boolean | Whether running in introspection mode where no filesystem changes should be made. When true, mods should only read and analyze files without writing. Used during config analysis and validation. | | config.modRequest.ignoreExistingNativeFiles | boolean | Whether to ignore existing native files. Used in template-based operations, particularly affects entitlements and other native configs to ensure alignment with prebuild expectations. |

When to use a dangerous mod

Use a dangerous mod when: (1) the modification cannot be made with a standard mod and existing plugins like withAndroidManifest or withPodfile do not support it, or a library requires specific native modifications not covered by standard plugins; (2) targeting an older Expo SDK version that doesn't include the needed mod plugin; (3) needing to perform intricate text manipulations with regexes or replace functions that existing mod plugins do not support.

withCustomPodfile example using withDangerousMod

Example config plugin that adds a CocoaPod dependency to ios/Podfile during prebuild: ```tsx import { ConfigPlugin, IOSConfig, withDangerousMod } from 'expo/config-plugins'; import fs from 'fs/promises'; import path from 'path'; const withCustomPodfile: ConfigPlugin = config => { return withDangerousMod(config, [ 'ios', async config => { const podfilePath = path.join(config.modRequest.platformProjectRoot, 'Podfile'); try { let contents = await fs.readFile(podfilePath, 'utf8'); const projectName = IOSConfig.XcodeUtils.getProjectName(config.modRequest.projectRoot); contents = addCustomPod(contents, projectName); await fs.writeFile(podfilePath, contents); console.log('✅ Successfully added custom pod to Podfile'); } catch (error) { console.warn('⚠️ Podfile not found, skipping modification'); } return config; }, ]); }; function addCustomPod(contents: string, projectName: string): string { if (contents.includes("pod 'Alamofire'")) { console.log('Alamofire pod already exists, skipping'); return contents; } const targetRegex = new RegExp( `(target ['"]${projectName}['"] do[\\s\\S]*?use_expo_modules!)`, 'm' ); return contents.replace(targetRegex, `$1\n pod 'Alamofire', '~> 5.6'`); } export default withCustomPodfile; ``` This plugin adds the Alamofire CocoaPod dependency after the use_expo_modules! statement in the Podfile, running during prebuild before CocoaPod dependencies are installed.

Considerations when using a dangerous mod

When using a dangerous mod, consider: (1) Limited idempotency guarantees—running the same dangerous mod multiple times may produce different results or cause issues; (2) Experimental and prone to breakage—test thoroughly with each SDK release as they are especially prone to breakage when native template changes occur; (3) Use standard mod plugins first—only use a dangerous mod when there are no existing mod plugins available to handle the use case; (4) Don't assume a file exists—always check the native directory and relative path before reading/writing; if using CNG, run npx expo prebuild to create native directories and manually verify file existence; (5) Dangerous mods run first—execution order of dangerous mods might be unreliable since they run before other modifiers, affecting build predictability and potentially causing conflicts.

withDangerousMod definition and purpose

Dangerous mods in Expo provide direct access to native project files through string manipulation and regular expressions. They serve as an escape hatch for modifications that cannot be achieved through existing mod plugins like withAndroidManifest or withPodfile.

withDangerousMod syntax and requirements

Using withDangerousMod requires: (1) a native platform (android or ios); (2) an asynchronous function that receives a config object with file system access; (3) relative file name/path to access inside the native directory; (4) reading the existing file, modifying its contents, and writing back to the file; (5) optionally logging custom messages for success and failure states when a plugin executes during the prebuild process.

Example gradle.properties configuration

Example showing gradle.properties usage: gradke.properties: ```properties expo.react.jsEngine=hermes ``` app/build.gradle: ```groovy project.ext.react = [enableHermes: findProperty('expo.react.jsEngine') ?: 'jsc'] ``` For keys in gradle.properties, use camel case separated by dots, usually starting with 'expo' prefix to denote that the property is managed by prebuild.

Modify Android gradle files via gradle.properties, not with regex

Instead of modifying Android Gradle files (build.gradle, settings.gradle) directly with withProjectBuildGradle, withAppBuildGradle, or withSettingsGradle mods, use the static gradle.properties file. The gradle.properties file is a static key/value file that Gradle can read. Use camel case keys separated by dots, usually prefixed with 'expo'. Access properties in Gradle using property() (throws error if missing) or findProperty() (doesn't throw, can use with ?: for defaults). Interact with Gradle files via Expo Autolinking instead for a programmatic interface.

Example config plugin using withStringsXml

Example showing a config plugin that safely modifies Android strings.xml using withStringsXml: ```js const { AndroidConfig, withStringsXml } = require('expo/config-plugins'); function withCustom(config, value) { return withStringsXml(config, config => { config.modResults = setStrings(config.modResults, value); return config; }); } function setStrings(strings, value) { return AndroidConfig.Strings.setStringItem( [ // XML represented as JSON // <string name="expo_custom_value" translatable="false">value</string> { $: { name: 'expo_custom_value', translatable: 'false' }, _: value }, ], strings ); } ``` With CNG, this is configured in app.json like: { "expo": { "plugins": [["expo-custom", "I Love Expo"]] } }

Config plugin dependencies in package.json

A library providing a config plugin should have these dependency declarations: dependencies (empty or minimal), devDependencies with expo at a specific version like "^50.0.0", peerDependencies with expo at >=50.0.0 or similar, and peerDependenciesMeta marking expo as optional. For simple plugins depending on stable APIs like AndroidManifest.xml or Info.plist modifications, a loose version constraint in peerDependencies is acceptable.

createRunOncePlugin prevents duplicate plugin execution

Use createRunOncePlugin to prevent duplicate plugins from running during migration from legacy UNVERSIONED plugins to versioned plugins. It wraps withRunOnce and appends items to pluginHistory to track if a plugin has already been run. Example: createRunOncePlugin(withMyCoolPlugin, pkg.name, pkg.version) where the version parameter is optional and defaults to UNVERSIONED if omitted.

Import config plugins from expo package, not separately

Always import expo/config-plugins and expo/config through the expo package re-exports: const { ... } = require('expo/config-plugins') and const { ... } = require('expo/config'). This ensures you are using the version depended on by the expo package. Importing separately risks getting an incompatible version due to module hoisting differences or failing to import at all with "plug and play" package managers like Yarn Berry or pnpm. Config types are exported directly from expo/config, so there is no need to install or import from expo/config-types.

Config plugin best practices for mods

Best practices for mods: (1) Avoid regex; use static modification instead. For Android gradle files, use gradle.properties. For Podfile modifications, use JSON that the Podfile reads. (2) Avoid long-running tasks like network requests or installing Node modules in mods. (3) Do not add interactive terminal prompts in mods. (4) Generate, move, and delete files only in dangerous mods, otherwise introspection will break. (5) Utilize built-in config plugins like withXcodeProject to minimize the number of times a file is read and parsed. (6) Use the same XML parsing libraries that prebuild uses internally to avoid unnecessary code rearrangement.

Modify iOS AppDelegate safely with AppDelegate subscribers

Modules needing to add delegate methods to AppDelegate should use AppDelegate subscribers instead of the dangerous withAppDelegate mod. AppDelegate subscribers allow native Expo modules to react to important events in a safe and reliable way. Examples include expo-linking (openURL), expo-notifications (didRegisterForRemoteNotificationsWithDeviceToken, didFailToRegisterForRemoteNotificationsWithError, didReceiveRemoteNotification).

VS Code Expo Tools extension for plugin development

The Expo Tools VS Code extension provides automatic validation on config plugins, surfaces error information, and provides quality of life improvements for config plugin development. It can be installed from the VS Code marketplace.

Debug config plugins with EXPO_DEBUG environment variable

Debug config plugins by running EXPO_DEBUG=1 expo prebuild. When EXPO_DEBUG is enabled, the plugin stack logs print showing which mods ran and their order. 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 are usually related to versioning issues. Running npx expo prebuild --clean removes generated native directories before compiling. Running npx expo config --type prebuild prints plugin results with mods unevaluated (no code generated). Expo CLI commands can be profiled using EXPO_PROFILE=1.

Plugin properties must be static values

Plugin properties used to customize plugins during prebuild must always be static values (no functions or promises). Valid static values are: boolean, number, string, null, arrays of static values, or objects with string keys mapping to static values. Static properties are required because the app config must be serializable to JSON for use as the app manifest. Attempt to make plugins work without props to help resolution tooling like expo install, and use good default values over mandatory configuration when feasible.

Example of modifying AndroidManifest.xml with a config plugin

Example showing how to add a meta-data item to the default application in AndroidManifest.xml: ```ts import { AndroidConfig, ConfigPlugin, withAndroidManifest } from 'expo/config-plugins'; import { ExpoConfig } from 'expo/config'; const { addMetaDataItemToMainApplication, getMainApplicationOrThrow } = AndroidConfig.Manifest; export const withMyCustomConfig: ConfigPlugin = config => { return withAndroidManifest(config, async config => { config.modResults = await setCustomConfigAsync(config, config.modResults); return config; }); }; async function setCustomConfigAsync( config: Pick<ExpoConfig, 'android'>, androidManifest: AndroidConfig.Manifest.AndroidManifest ): Promise<AndroidConfig.Manifest.AndroidManifest> { const appId = 'my-app-id'; const mainApplication = getMainApplicationOrThrow(androidManifest); addMetaDataItemToMainApplication( mainApplication, 'my-app-id-key', appId ); return androidManifest; } ``` Splitting the modification logic into a separate function makes it easier to test. Splitting out the mod makes it easier to test.

Versioned vs UNVERSIONED plugins in pluginHistory

In pluginHistory, plugins show either a version number (like '11.0.0') or 'UNVERSIONED'. Versioned plugins use the plugin from node_modules/{package}/app.plugin.js. UNVERSIONED plugins use legacy plugins shipped with expo-cli for backward compatibility. For the most stable experience, have no UNVERSIONED plugins in your project, as UNVERSIONED plugins may not support the native code in your project and breaking changes could break prebuild.

Legacy plugins are automatically applied when installed

Legacy plugins are automatically applied to a project when installed, even if not manually added to plugins in app.json. For example, if expo-camera is installed but not in the plugins array, Expo CLI automatically adds it to ensure required permissions are added. Users can customize legacy plugins by manually adding them to the plugins array, and manually defined plugins take precedence over automatic plugins. Debug which plugins were automatically added by running expo config --type prebuild and checking the _internal.pluginHistory property.

Introspection reads evaluated modifier results without generating code

Introspection is an advanced debugging technique that reads evaluated modifier results without generating any code in the project. It allows quick debugging of static modifications without running prebuild. Run expo config --type introspect in a project to try introspection. Introspection works by creating custom base mods that don't write modResults to disk; instead they save results to _internal.modResults.{modName}. Introspection only supports safe modifiers (static files: JSON, XML, plist, properties) and these mods: android.manifest, android.gradleProperties, android.strings, android.colors, android.colorsNight, android.styles, ios.infoPlist, ios.entitlements, ios.expoPlist, ios.podfileProperties. The preview feature in vscode-expo provides live introspection interaction.

Use modifier previews to debug plugins live

Modifier previews in the VS Code Expo extension allow you to debug the results of your plugin live without running prebuild.

Manually test a plugin without a monorepo

To manually test a plugin: (1) Run npm pack in the package with the config plugin. (2) In your test project, run npm install path/to/react-native-my-package-1.0.0.tgz. (3) Add the package to the plugins array in app.json: { "plugins": ["react-native-my-package"] }. If VS Code Expo Tools is installed, autocomplete should work for the plugin. (4) To update the package, change the version in the package's package.json and repeat the process.

Use AndroidManifest.xml merging system before config plugins

Packages should attempt to use Android's built-in AndroidManifest.xml merging system before using a config plugin. This works for static, non-optional features like permissions. The advantage is features are merged at build-time (not prebuild-time), minimizing the possibility of configuration being missed if users forget to prebuild. The drawback is users cannot use introspection to preview changes or debug potential issues.

Do not directly modify iOS Podfile with config plugins

The iOS Podfile is a Ruby file and cannot be safely modified from Expo config plugins. Opt for another approach such as Expo Autolinking hooks. The only safe mechanism is to interact with the static JSON file Podfile.properties.json using the ios.podfileProperties mod or withPodfileProperties modifier. This is used by expo-build-properties and to configure the JavaScript engine.

Plugin versioning across SDK upgrades

When Expo SDK upgrades to a new version of React Native, the template may change significantly. If a plugin uses static modifications, it usually works well across SDK versions. If a plugin uses regular expressions to transform application code, document which Expo SDK version the plugin is intended for. During the Expo SDK release cycle, there is a beta period where you can test if your plugin works with the new SDK version before it is released.

Example of modifying Info.plist with a config plugin

Example showing how to add a property to Info.plist: ```ts import { ConfigPlugin, withInfoPlist } from 'expo/config-plugins'; export const withCustomConfig: ConfigPlugin<string> = (config, id) => { return withInfoPlist(config, config => { config.modResults.GADApplicationIdentifier = id; return config; }); }; ``` Using withInfoPlist is safer than statically modifying expo.ios.infoPlist in app.json because it reads the Info.plist contents and merges with expo.ios.infoPlist, helping prevent changes from being overwritten.

expo install automatically adds config plugins

When a node module is installed with npx expo install and it includes a config plugin, it is automatically added to the project's app config. This makes setup easier and helps prevent users from forgetting to add plugins. Caveats: (1) expo install only automatically adds config plugins to static app.json or app.config.json files, not to dynamic app.config.js (to prevent packages like lodash from being mistaken as config plugins). (2) There is no mechanism for detecting if a config plugin has mandatory props, so expo install will only add the plugin without attempting to add required props. (3) For dynamic configs, users see a warning with instructions to manually add the plugin to their app config.

Create custom base modifiers for managing custom files

To manage custom files beyond what @expo/prebuild-config supports, add custom base modifiers locally using BaseMods.withGeneratedBaseMods. You provide a platform, a providers object with a custom modifier name as key, and a BaseMods.provider configuration with getFilePath, read, and write methods. Base mods MUST be added last in the plugins array after all other plugins that use the mod, so they write results to disk at the end. Example: adding ios.appDelegateHeader to manage AppDelegate.h files.

Example custom base modifier for AppDelegate.h

Example showing a custom base modifier for ios/*/AppDelegate.h: ```ts import { ConfigPlugin, IOSConfig, Mod, withMod, BaseMods } from 'expo/config-plugins'; import fs from 'fs'; export function withAppDelegateHeaderBaseMod(config) { return BaseMods.withGeneratedBaseMods<'appDelegateHeader'>(config, { platform: 'ios', providers: { appDelegateHeader: BaseMods.provider<IOSConfig.Paths.AppDelegateProjectFile>({ getFilePath({ modRequest: { projectRoot } }) { const filePath = IOSConfig.Paths.getAppDelegateFilePath(projectRoot); if (filePath.endsWith('.m')) { return filePath.substr(0, filePath.lastIndexOf('.')) + '.h'; } throw new Error(`Could not locate a valid AppDelegate.h at root: "${projectRoot}"`); }, async read(filePath) { return IOSConfig.Paths.getFileInfo(filePath); }, async write(filePath: string, { modResults: { contents } }) { await fs.promises.writeFile(filePath, contents); }, }), }, }); } export const withAppDelegateHeader: ConfigPlugin<Mod<IOSConfig.Paths.AppDelegateProjectFile>> = ( config, action ) => { return withMod(config, { platform: 'ios', mod: 'appDelegateHeader', action, }); }; ``` Add base mods to the plugins array AFTER all other plugins using them.

Set up a monorepo for plugin development with TypeScript and Jest

To develop plugins with TypeScript and Jest tests, set up a monorepo. Create a module in the monorepo's packages/ directory and bootstrap a config plugin in it. Expo config plugins have full monorepo support built-in, enabling you to work on a node module and import it in your app config like a published npm package.

Customize iOS Podfile safely with Podfile.properties.json

Customize the iOS Podfile safely using the static Podfile.properties.json file instead of regex modifications. Podfile.properties.json is a JSON file that the Podfile reads at runtime. Use the withPodfileProperties or ios.podfileProperties mod to customize the file. The Podfile should parse this JSON: require 'json'; podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}. Then access values like podfile_properties['ios.deploymentTarget']. This is more reliable than regex because changes compose well and multiple changes won't collide. Generally, only interact with Podfile via Expo Autolinking.

AndroidManifest.xml example with namespace declaration

Example of a package's AndroidManifest.xml that injects a required permission: ```xml <manifest package="expo.modules.filesystem" xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.INTERNET"/> </manifest> ``` The xmlns:android="http://schemas.android.com/apk/res/android" namespace declaration is required to use android:* properties like android:name in your manifest.

Use ReactActivityLifecycleListeners for Android app startup configuration

For Android app startup configuration before the JS engine starts (like setting resize mode in MainActivity.java), use the ReactActivityLifecycleListeners interface from expo-modules-core instead of dangerously regexing MainActivity. This system includes: (1) ReactActivityLifecycleListeners interface to get a native callback when ReactActivity.onCreate is invoked, (2) withStringsXml mod to write properties to Android strings.xml file, (3) SingletonModule interface (optional) to create a shared interface between native modules and ReactActivityLifecycleListeners. This ensures features work safely across all supported Android languages (Java, Kotlin), Expo versions, and combinations of config plugins.

Example Kotlin ReactActivityLifecycleListener implementation

Example showing how to implement a ReactActivityLifecycleListener in Kotlin: CustomPackage.kt: ```kotlin package expo.modules.custom import android.content.Context import expo.modules.core.BasePackage import expo.modules.core.interfaces.ReactActivityLifecycleListener class CustomPackage : BasePackage() { override fun createReactActivityLifecycleListeners(activityContext: Context): List<ReactActivityLifecycleListener> { return listOf(CustomReactActivityLifecycleListener(activityContext)) } } ``` CustomReactActivityLifecycleListener.kt: ```kotlin package expo.modules.custom import android.app.Activity import android.content.Context import android.os.Bundle import expo.modules.core.interfaces.ReactActivityLifecycleListener class CustomReactActivityLifecycleListener(activityContext: Context) : ReactActivityLifecycleListener { override fun onCreate(activity: Activity, savedInstanceState: Bundle?) { var value = getValue(activity) if (value != "") { // Do something to the Activity that requires the static value... } } private fun getValue(context: Context): String = context.getString(R.string.expo_custom_value).toLowerCase() } ``` Naming convention: node module name (expo-custom) plus value name (value) using underscores as delimiters: expo_custom_value. For @expo/vector-icons + iconName, use expo__vector_icons_icon_name.

Split config plugins by platform for easier debugging

When using functions within the config plugin, split them by platform. For example, create separate withAndroidSplash and withIosSplash functions. This makes using the --platform flag in npx expo prebuild easier to follow in EXPO_DEBUG mode, as the logging will show which platform-specific functions are being executed.

TypeScript is preferable to JavaScript for config plugins

A TypeScript plugin is always preferable to a JavaScript plugin due to added type-safety. Use the expo-module-scripts plugin tooling to support TypeScript compilation for your config plugin.

Use memfs for filesystem-dependent config plugin tests

When a config plugin requires access to the filesystem, use a mock system like memfs for testing instead of the actual filesystem. The expo-notifications plugin is a good example of this pattern, with tests in the plugin/__tests__ directory and __mocks__ directory setup.

Use try-catch blocks in config plugins for graceful error handling

Config plugins should use try-catch blocks to intercept errors early and provide clear feedback when a configuration fails. Within the try-catch, validate configuration early before applying any transformations, then apply platform-specific configurations. When catching errors, re-throw with additional context if needed to help developers understand what went wrong.

Naming convention for config plugin functions

Use withFeatureName for the plugin function name if it applies to all platforms. If the plugin is platform-specific, use camel case naming with the platform right after "with". For example, withAndroidSplash or withIosSplash.

Config plugins should be idempotent when possible

Plugins should be idempotent, meaning the changes they make are the same whether they are run on a fresh native project template or run again on a project template where its changes already exist. This allows developers to run 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.

Document plugin properties in README, specifying which are required

The plugin README should document the available properties for the plugin, clearly specifying which properties are required and which are optional.

Document manual setup instructions in plugin README

If a config plugin is tied to a React Native module, document manual setup instructions for the package in the README. If anything goes wrong with the plugin, developers should be able to manually add the project modifications that were automated by the plugin. This also allows you to support projects that are not using Continuous Native Generation.

Alternative: Distribute config plugin as separate package

Some libraries distribute their config plugin as a separate package from their main library. This allows you to maintain your config plugin separately from the rest of your native module. The separate plugin package should have an app.plugin.js export and include files entry listing "app.plugin.js" and "build/" directory. It should have peerDependencies on both "expo" and the main library package.

Alternative: Add app.plugin.js to existing library using other build tools

For libraries using different build tools like those created with create-react-native-library, add an app.plugin.js file that requires the compiled plugin and build it along with your main package. This allows you to add plugin support without adopting expo-module-scripts.

Do not modify sdkVersion in config plugins

Do not modify the sdkVersion via a config plugin, as this can break commands like expo install and cause other unexpected issues.

Unit test config plugins with mocked configuration objects

Unit tests for config plugins should test configuration transformation logic with mocked Expo configuration objects. Use Jest to create mock configuration objects, pass them through the plugin, and verify expected modifications are made correctly. This approach avoids involving the file system during tests.

Config plugin structure receives configuration, applies transformations via mods, returns modified configuration

Every config plugin follows the same pattern: it receives configuration and parameters as input, applies transformations through mods (modifier functions), and returns the modified configuration. The plugin should export a ConfigPlugin function that takes a config object and optional props, applies platform-specific configurations using mod functions like withAndroidManifest and withInfoPlist, and returns the modified config.

app.plugin.js entry point exports compiled plugin from plugin/build

The app.plugin.js file in the library root should export the compiled plugin code: module.exports = require('./plugin/build'). The Expo CLI looks for this file in the project root of your library to find the plugin. The plugin/build directory contains the JavaScript files generated from your config plugin's TypeScript source code.

Plugin TypeScript configuration with expo-module-scripts

The plugin/tsconfig.json file should extend "expo-module-scripts/tsconfig.plugin" with compiler options setting outDir to "build" and rootDir to "src". The include array should contain "./src" and exclude should contain "**/__mocks__/*" and "**/__tests__/*".

Required package.json scripts and dependencies for expo-module-scripts

When using expo-module-scripts, the package.json must include these scripts: "build": "expo-module build", "build:plugin": "expo-module build plugin", "clean": "expo-module clean", "test": "expo-module test", "prepare": "expo-module prepare", and "prepublishOnly": "expo-module prepublishOnly". The devDependencies should include "expo": "^{{expoSdkVersion}}". The peerDependencies should include "expo": ">={{expoSdkVersion}}" with peerDependenciesMeta marking expo as optional.

Use expo and expo-module-scripts for config plugin development

The most straightforward approach to leverage Expo's tooling for config plugins 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 and handles TypeScript compilation.

Check existing app config and prebuild config before writing plugins

Before writing a config plugin, check if there is already a configuration available in the app config or prebuild config. If there is, you don't need to write a config plugin for it.

Recommended directory structure for config plugins in libraries

A library with a config plugin should use the following directory structure: ./android for Android native module code, ./ios for iOS native module code, ./src/index.ts as main library entry point, ./src/YourAwesomeLibrary.ts for core library implementation, ./src/types.ts for TypeScript type definitions, ./plugin/src/index.ts as plugin entry point, ./plugin/src/withAndroid.ts for Android-specific configurations, ./plugin/src/withIos.ts for iOS-specific configurations, ./plugin/build/ for compiled plugin output (generated), ./plugin/__tests__/ for plugin-specific tests, ./plugin/tsconfig.json for plugin-specific TypeScript config, ./example/app.json for example app configuration, ./example/App.tsx for example app implementation, ./example/package.json for example app dependencies, ./__tests__/ for main library tests, ./app.plugin.js as plugin entry point for Expo CLI, ./package.json for package configuration, ./tsconfig.json for main TypeScript configuration, ./jest.config.js for testing configuration, and ./README.md for documentation.

Config plugins enable Continuous Native Generation workflow compatibility

Config plugins enable compatibility with Continuous Native Generation (CNG), 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.

Config plugins automate native configuration instead of manual file editing

Config plugins represent a transformative approach to automating native project configuration. Rather than requiring library users to manually edit native files such as AndroidManifest.xml and Info.plist, 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.

Config plugin definition and purpose

A config plugin is a top-level custom configuration point that is not built into the app config. Using a config plugin, you can modify native projects created during the prebuild process in Continuous Native Generation (CNG) projects. Config plugins allow you to automatically configure native projects beyond what can be configured using default app config props.

Config plugin configuration argument

Optionally, a second argument can be passed to a plugin to configure it.

Mods evaluation and execution phase

Mods are only evaluated during the syncing phase of `npx expo prebuild` and modify native files during code generation. Any modifications made to app config in a config plugin should be outside of a mod to ensure execution in non-prebuild configuration scenarios.

Give your agent this brain