skip to content

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?

level: middleimportance: nice to knowfreq 16%

answer

  1. paths load as-is
  2. package: app.plugin at root
  3. else the package main entry
  4. Node-only entry, not runtime code
  5. expo install adds it for you

basics

~20 s

A 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
javascript
// node_modules/expo-location/app.plugin.js
module.exports = require('./plugin/build/withLocation');

go deeper

for a junior

Recall that a plugins entry can be a package name or a local path, and that libraries expose their plugin through app.plugin.js.

for a middle

Explain the resolution order, why a separate Node-only entry exists, and what errors appear when a package has no plugin.

for a senior

Avoid deep imports, verify that installs added the plugins you rely on, and review what each plugin will change before adding it.

for a principal

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