When an Expo app config lists a library name in plugins, how does Expo find that library's config plugin, and why do libraries ship app.plugin.js?
answer
- paths load as-is
- package: app.plugin at root
- else the package main entry
- Node-only entry, not runtime code
- expo install adds it for you
basics
~20 sA path loads as-is; a package name resolves to app.plugin.js (or .cjs, .mjs, .ts, .cts, .mts) at the package root, else the package's main entry. app.plugin.js keeps Node-run config code separate from the runtime code Metro bundles.
solid answer
~40 s`@expo/config-plugins` resolves a string entry in order: a relative path or file specifier loads that file; a package name looks for `app.plugin` at the package root with `.js`, `.cjs`, `.mjs`, `.ts`, `.cts` or `.mts`; failing that, the package's `main` entry is used, and if it does not export a function prebuild fails saying the package does not contain a valid config plugin. `app.plugin.js` exists because the app config is evaluated by Node.js, while the `main` entry is React Native runtime code for Metro. `expo-location`'s `app.plugin.js`, for instance, just re-exports its compiled plugin. So use the package name rather than a deep path, and `npx expo install` adds packages that ship a plugin file to `plugins` for you.
code
javascript · 2 lines// node_modules/expo-location/app.plugin.js
module.exports = require('./plugin/build/withLocation');go deeper
Recall that a plugins entry can be a package name or a local path, and that libraries expose their plugin through app.plugin.js.
Explain the resolution order, why a separate Node-only entry exists, and what errors appear when a package has no plugin.
Avoid deep imports, verify that installs added the plugins you rely on, and review what each plugin will change before adding it.
Set standards for internal plugin packages: a Node-only entry, compiled output, validated options and idempotent edits.
## How a plugin entry becomes a function An entry in the Expo app config's `plugins` array is usually a **string**. Before prebuild can run it, `@expo/config-plugins` must turn that string into a JavaScript function. The resolver follows a fixed order: 1. **A file reference is loaded as-is.** A relative path such as `./plugins/withMapsKey.js`, or a specifier with a file path such as `some-package/plugin.js`, resolves directly to that file. 2. **A package name looks for `app.plugin` at the package root.** For `expo-location`, the resolver tries `expo-location/app.plugin` with the extensions `.js`, `.cjs`, `.mjs`, `.ts`, `.cts` and `.mts`, in that order. 3. **Otherwise it falls back to the package's `main` entry.** If there is no `app.plugin` file, the package's normal entry point is loaded and treated as the plugin. If nothing resolves, prebuild fails with a "Failed to resolve plugin for module" error that asks whether node modules are installed. If a package resolves but does not export a function, the error says the package "does not contain a valid config plugin". ## Why `app.plugin.js` exists A library's `main` entry is **runtime code**: React Native components, imports of native modules, often ESM or syntax that needs Metro's transforms. The app config is evaluated by **Node.js** at build time. Loading that runtime entry in Node would often crash, and even when it works, it is the wrong code. `app.plugin.js` gives the library a separate, **Node-only entry** for configuration. `expo-location`, for example, ships an `app.plugin.js` that simply re-exports its compiled plugin: ```js module.exports = require('./plugin/build/withLocation'); ``` So the same package name means two different things in two places: | Where the name appears | What loads | |---|---| | `import * as Location from 'expo-location'` in app code | The runtime entry, bundled by Metro for the device | | `"expo-location"` in the `plugins` array | `app.plugin.js`, run by Node during config evaluation | ## Consequences for app developers - **Use the package name, not a deep path.** Pointing `plugins` at a file inside the package, such as its build folder, bypasses the resolution order; Expo documents this as not recommended because internal paths can change between versions. - **A package without a plugin cannot simply be listed.** If its main entry does not export a plugin function, prebuild fails, so check the library's documentation before adding it. - **`npx expo install` helps.** When the installed package ships an `app.plugin` file, `npx expo install` adds it to the `plugins` array (without options) if it is not listed already, skipping the few plugins Expo applies automatically. A plain package-manager install does not. - **Options still need the tuple.** `["expo-location", { ... }]` passes options; a bare object after the string fails with an error telling you to wrap the plugin configuration in square brackets. ## Consequences for library authors - **Ship `app.plugin.js` at the package root** and keep it free of React Native imports. - **Compile TypeScript plugins** to JavaScript before publishing, the way `expo-location` requires `plugin/build/...`; the resolver accepts `.ts` files, but a published package should not depend on the consumer being able to load TypeScript. - **Make the plugin idempotent**, because apps re-run prebuild. - **Validate options** and throw a clear error on bad input, so users do not debug a broken manifest instead. ## Local plugins in an app For a delivery app that needs a setting no library provides, a **local plugin** is just a file referenced by path: ```json { "expo": { "plugins": ["./plugins/withDeliveryZones.js"] } } ``` It resolves by rule 1, runs in Node, and follows the same ordering rules as package plugins. Writing its mods is a separate topic; applying it is only the path in the array. This resolution order is implemented in `@expo/config-plugins` in Expo SDK 57.
- Why is pointing plugins at a file inside a package's build folder discouraged?A deep path is loaded as-is, so it bypasses the `app.plugin` lookup the library intends you to use. Internal file layouts are not a public contract and can move between versions, breaking prebuild after an upgrade. Reference the package name and let the resolver find its `app.plugin` entry.
- What does npx expo install do about config plugins that a plain package-manager install does not?After installing, `npx expo install` checks whether each package ships an `app.plugin` file and, unless it is already listed or is one Expo applies automatically, adds the package name to the `plugins` array without options. A plain package-manager install leaves the app config untouched, so a library's plugin is silently not applied until you add it.
saying these in an interview costs you the question
- Expo always loads a package's main entry as its plugin
- Every installed package with a plugin is applied automatically
- A deep path into build/ is the recommended reference
- app.plugin.js runs on the device with the app
- Any package can be listed in plugins safely