npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@callstack/repack-expo

v0.0.1

Published

First-class Expo integration for Re.Pack and Rspack

Readme

@callstack/repack-expo

First-class Expo integration for Re.Pack and Rspack.

This package is in alpha while the Expo v1 release gates are being completed. Install it from the alpha tag (@callstack/repack-expo@alpha) and expect breaking changes between releases.

Requirements

  • Expo SDK 56 or newer
  • iOS or Android
  • React Native New Architecture
  • Hermes
  • Rspack
  • Node.js supported by the selected Expo SDK (20.19+ minimum)
  • Expo prebuild or CNG

SDK 56 is the minimum supported contract. The integration does not hard-code an upper Expo version allowlist: it validates the required generated-native seams and fails with recovery guidance when an Expo template is incompatible.

Quick start

Run the initializer from the root of an existing Expo application with a static app.json or package.json#expo configuration:

npx @callstack/repack-expo init

Run the dependency command printed by init, then regenerate the native projects and validate the result:

npx expo prebuild --clean
npx @callstack/repack-expo doctor

Start Re.Pack in one terminal:

npm run repack:start

Build and launch the generated development application in another terminal:

npm run repack:ios
# or
npm run repack:android

The generated scripts use expo run:* --no-bundler. Do not start Metro or use expo start for this workflow.

The initializer detects npm, yarn, pnpm and bun. Replace npm run in the examples with the equivalent command for the application's package manager.

Safe initializer modes

npx @callstack/repack-expo init --check
npx @callstack/repack-expo init --dry-run
npx @callstack/repack-expo init --dry-run --json

--check exits unsuccessfully when managed files are not current. --dry-run returns the planned changes without writing them. --json produces machine-readable diagnostics. An existing incompatible Rspack configuration is never overwritten unless --force is explicitly supplied.

Executable app.config.js, app.config.ts, app.config.mjs and app.config.cjs files are intentionally not rewritten. Convert the required configuration to a static Expo config or apply the diagnostic's recovery steps before rerunning init.

What init manages

init updates only application-owned configuration files:

| File | Managed result | | --- | --- | | package.json | Adds compatible Re.Pack, Expo integration, Community CLI and Rspack development dependencies, plus repack:start, repack:ios and repack:android scripts. | | app.json or package.json#expo | Registers @callstack/repack-expo and disables Expo Updates. SDK 56+ New Architecture and Hermes defaults are not written redundantly. | | react-native.config.js | Registers the existing Re.Pack Rspack CLI commands. | | rspack.config.mjs | Adds a minimal application config containing one ExpoPlugin. Existing compatible configs are preserved. |

The command never edits ios/ or android/. Expo prebuild invokes the Config Plugin and owns all generated native changes. Rerun prebuild after changing the Config Plugin version or options; do not copy or maintain the generated Swift, Objective-C, Kotlin, Gradle or Xcode snippets by hand.

The generated Rspack shape is intentionally small:

import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { ExpoPlugin } from '@callstack/repack-expo/rspack';

const projectRoot = path.dirname(fileURLToPath(import.meta.url));

export default (env) => {
  const {
    mode = 'development',
    platform = process.env.PLATFORM,
    devServer,
  } = env;

  if (platform !== 'ios' && platform !== 'android') {
    throw new Error('ExpoPlugin requires PLATFORM=ios or android');
  }

  return {
    context: projectRoot,
    devServer,
    mode,
    name: platform,
    output: {
      clean: true,
      path: path.join(projectRoot, 'build', 'rspack', platform),
      uniqueName: 'expo-app',
    },
    plugins: [new ExpoPlugin({ platform })],
  };
};

Do not add a separate RepackPlugin, application setup.ts, React Native aliases, extraChunks configuration or ScriptManager resolver. ExpoPlugin owns those Re.Pack defaults and rejects a duplicate RepackPlugin.

Expo Router and plain entries

Expo Router applications keep the idiomatic entry:

{
  "main": "expo-router/entry"
}

For native Rspack builds, ExpoPlugin replaces Router's Metro-only bootstrap side effect and mounts the resolved Router application without importing the Metro runtime.

A non-Router application can use any resolvable package.json#main entry. The entry follows React Native platform extension precedence. Missing, ambiguous or Metro-virtual entries fail before compilation instead of falling back to Metro.

Custom native entry and Rspack config paths are Config Plugin options:

{
  "expo": {
    "plugins": [
      [
        "@callstack/repack-expo",
        {
          "entry": "src/main.ts",
          "configPath": "config/rspack.native.mjs"
        }
      ]
    ]
  }
}

configPath must resolve inside the Expo application when using init. entry can be a project-relative or absolute resolvable path. init, prebuild and doctor read the same options.

Development and Fast Refresh

The development server is ordinary Re.Pack:

npm run repack:start

The generated iOS and Android Debug projects request the index bundle from that server. expo run:ios --no-bundler and expo run:android --no-bundler install and launch the native clients without starting Metro.

Fast Refresh follows normal Re.Pack semantics. Updating a React component can preserve parent and runtime state. Editing the application bootstrap or another refresh boundary can trigger a full JavaScript reload. Source maps and Hermes symbolication retain application source paths.

Expo Go, the expo-dev-client launcher/QR protocol and Expo development manifests are not part of this integration.

Local release builds

After prebuild, standard native Release builds invoke Community CLI's bundle command. The registered Re.Pack commands dispatch that request to Rspack; no separate JavaScript export command is required.

Expo CLI 57 performs an eager Metro export for expo run:ios --configuration Release and expo run:android --variant release, even with --no-bundler. Build with Xcode or Gradle directly instead. Once built, expo run with --binary <path> --no-bundler can install and launch the existing binary without that export step.

Use the Re.Pack Expo CLI to build natively (Release is the default and currently the only configuration):

npx repack-expo build --platform android
npx repack-expo build --platform ios
# Also install and launch the built application:
npx repack-expo build --platform android --run
npx repack-expo build --platform ios --run --device <simulator-id>

Run npx expo prebuild --platform android or --platform ios in the app directory first, and install iOS pods (cd ios && pod install) if needed. Build the workspace packages first when using this repository. The command never regenerates native projects automatically. Android currently supports macOS/Linux and the standard app module's Release APK (no custom flavors or APK splits). iOS requires macOS/Xcode and targets the simulator using the host architecture, not a signed device build or archive. It discovers the workspace and scheme; use --scheme <name> when the scheme is ambiguous. The output .app path is read from Xcode build settings rather than inferred from the scheme name. --run requires an Android device/emulator or an iOS simulator. --device selects the install target using Expo's device selector: an Android device/AVD name (not the adb serial), or an iOS simulator name/UDID. It requires --run.

The repository host tester-expo exposes repack:release:android and repack:release:ios as aliases, for example pnpm --filter tester-expo repack:release:ios --run. The command does not start a dev server or build/serve Module Federation remotes; remote widgets still need their separate production build and HTTP server.

tester-expo-widget does not provide standalone Release aliases. Its current configuration publishes nested lazy chunks as remote files, while standalone Release execution looks for those chunks inside the application and fails with ChunkLoadError. Standalone Release support is deferred; use repack:build:production to publish the widget and load it from tester-expo. The remote-in-host workflow has been validated on Android and iOS.

Xcode and Gradle remain responsible for Hermes compilation, final source-map composition, native assets and application packaging. expo export is not a Re.Pack release path.

EAS Build

EAS must not run its eager Metro-compatible JavaScript step before the native build. Add this environment variable to every Re.Pack EAS profile:

{
  "build": {
    "production": {
      "env": {
        "EAS_BUILD_DISABLE_BUNDLE_JAVASCRIPT_STEP": "1"
      }
    }
  }
}

Then run the normal EAS command:

npx eas build --platform android --profile production
npx eas build --platform ios --profile production

With CNG, EAS generates the absent native project and runs the registered Config Plugin. If native projects are committed, run expo prebuild --clean after every integration/config change and commit the regenerated projects before EAS Build. Never reproduce the Config Plugin's native edits manually.

The repository's pnpm fixture additionally builds private workspace packages in postinstall and disables the generic Expo doctor because that doctor reports duplicate-module false positives for this workspace layout. Applications consuming a published package do not need the private-workspace build step. Disable the generic Expo doctor only if it reports that same pnpm duplicate false positive, and use npx @callstack/repack-expo doctor to validate the generated Re.Pack seams.

Expo Updates and EAS Update are unsupported in v1. init requires expo.updates.enabled to be false; an EAS native build is required after application, integration, dependency or native fingerprint changes.

Assets, fonts and local chunks

ExpoPlugin installs Expo-compatible handling for static images, density variants, fonts and XML resources. Existing compatible Rspack asset rules are preserved. Android production fonts are emitted as raw resources so expo-asset and expo-font can resolve their packaged resource identifiers.

Dynamic imports produce flat local chunks using [name].chunk.bundle by default. Release builds embed those chunks in the application, and the existing Re.Pack ScriptManager loads them from the application filesystem:

const LazyCard = React.lazy(() => import('./LazyCard'));

Development chunks are loaded from the Re.Pack development server. Release chunks do not require that server or an internet connection. Nested, constant or otherwise colliding chunk filenames fail during configuration.

This packaged-local behavior remains the default when Module Federation v2 is enabled. A host can therefore use a local React.lazy() component and remote widgets in the same application without turning the local import into a remote chunk.

Module Federation v2

Module Federation is an explicit, application-owned Re.Pack capability. It is not generated by init and does not add an Expo-specific runtime. Install the @module-federation/enhanced version required by the application's Re.Pack version, then use only the public Re.Pack v2 wrapper:

npm install @module-federation/enhanced

Do not instantiate @module-federation/enhanced/rspack directly. MF v1 and Metro Module Federation are different runtimes and are not supported by this integration.

Configure the host

Keep the host's ExpoPlugin unchanged and add the ordinary Re.Pack v2 plugin. The order of the two plugins does not matter:

import path from 'node:path';
import { fileURLToPath } from 'node:url';
import * as Repack from '@callstack/repack';
import { ExpoPlugin } from '@callstack/repack-expo/rspack';

const projectRoot = path.dirname(fileURLToPath(import.meta.url));

export default (env) => {
  const {
    mode = 'development',
    platform = process.env.PLATFORM,
    devServer,
  } = env;

  if (platform !== 'ios' && platform !== 'android') {
    throw new Error('Expo host requires PLATFORM=ios or android');
  }

  const widgetManifest =
    `ExpoWidget@https://cdn.example.com/widgets/${platform}/mf-manifest.json`;

  return {
    context: projectRoot,
    devServer,
    mode,
    name: platform,
    output: {
      clean: true,
      path: path.join(projectRoot, 'build', 'rspack', platform),
      uniqueName: `expo-host-${platform}`,
    },
    plugins: [
      new ExpoPlugin({ platform }),
      new Repack.plugins.ModuleFederationPluginV2({
        name: 'ExpoHost',
        dts: false,
        remotes: {
          ExpoWidget: widgetManifest,
        },
        shared: {
          react: { singleton: true, eager: true },
          'react-native': { singleton: true, eager: true },
          'expo-constants': { singleton: true, eager: true },
          'expo-asset': { singleton: true, eager: true },
          'expo-font': { singleton: true, eager: true },
        },
      }),
    ],
  };
};

The host leaves ExpoPlugin.repack.extraChunks absent, so the default local chunk rule remains active. ModuleFederationPluginV2 registers its remote resolvers at normal Re.Pack priority, while the Expo local resolver remains a fallback for unmatched application chunks. Both paths use the same ScriptManager instance.

The host owns Expo Router and decides where the remote UI is mounted. A widget can expose a component, screen or navigator, but its file routes are not merged into the host's Router tree.

Configure an Expo widget

An Expo widget is an independent Expo project with a normal synchronous entry for standalone development and an MF v2 exposure. Its production config sends the exposure, nested lazy chunks and remote assets to a deployable platform directory:

import path from 'node:path';
import { fileURLToPath } from 'node:url';
import * as Repack from '@callstack/repack';
import { ExpoPlugin } from '@callstack/repack-expo/rspack';

const projectRoot = path.dirname(fileURLToPath(import.meta.url));

export default (env) => {
  const {
    mode = 'development',
    platform = process.env.PLATFORM,
    devServer,
  } = env;

  if (platform !== 'ios' && platform !== 'android') {
    throw new Error('Expo widget requires PLATFORM=ios or android');
  }

  const remoteOutputPath = path.join(
    projectRoot,
    'build',
    'remote',
    platform
  );
  const remotePublicPath =
    `https://cdn.example.com/widgets/${platform}/remote-assets`;

  return {
    context: projectRoot,
    devServer,
    mode,
    name: platform,
    module: {
      rules:
        mode === 'production'
          ? Repack.getAssetTransformRules({
              remote: { publicPath: remotePublicPath },
            })
          : [],
    },
    output: {
      clean: true,
      path: path.join(projectRoot, 'build', 'rspack', platform),
      publicPath: `https://cdn.example.com/widgets/${platform}/`,
      uniqueName: `expo-widget-${platform}`,
    },
    plugins: [
      new ExpoPlugin({
        platform,
        repack: {
          output: { auxiliaryAssetsPath: remoteOutputPath },
          extraChunks: [
            { include: /.*/, type: 'remote', outputPath: remoteOutputPath },
          ],
        },
      }),
      new Repack.plugins.ModuleFederationPluginV2({
        name: 'ExpoWidget',
        filename: 'ExpoWidget.container.js.bundle',
        exposes: { './Widget': './src/Widget' },
        dts: false,
        shared: {
          react: { singleton: true, eager: true },
          'react-native': { singleton: true, eager: true },
          'expo-constants': { singleton: true, eager: true },
          'expo-asset': { singleton: true, eager: true },
          'expo-font': { singleton: true, eager: true },
        },
      }),
    ],
  };
};

Passing extraChunks follows ordinary Re.Pack replacement semantics. The widget intentionally replaces the packaged-local default so every nested widget import is published remotely; the host does not pass this option and keeps its own lazy chunks packaged.

A widget cannot deliver native dependencies. Every native module used by the widget must already be installed and linked in the host binary, and its JavaScript package must be shared compatibly. Adding or changing a native dependency requires a new host native build. The existing Config Plugin remains the sole owner of generated native changes, and Expo prebuild continues to generate the native directories.

Develop host and widget

The widget and host use existing Re.Pack CLI commands on separate ports. From the widget project, start its remote development server:

npm run repack:start:remote

From the host project, start the normal server and launch the generated native client:

npm run repack:start
npm run repack:ios
# or
npm run repack:android

The validated widget script maps repack:start:remote to react-native webpack-start --port 8082. There is no Expo-specific widget CLI, async bootstrap requirement or Metro server. Ordinary Re.Pack Fast Refresh and reload boundaries apply.

Load, fail and retry

Mount the exposure through the public MF v2 runtime:

import { loadRemote } from '@module-federation/enhanced/runtime';

const widgetModule = await loadRemote('ExpoWidget/Widget');

The application should immediately handle the returned promise, store a rejection and throw that error during render into its own React error boundary. Retry by starting a new loadRemote() attempt. This preserves the original Module Federation or ScriptManager diagnostic for missing manifests, scripts and nested chunks; widget image and font failures can use the same boundary. Do not replace this path with a Webpack remote import() or a global error handler.

Build and deploy a widget

Build each platform independently with the standard Re.Pack bundle (or webpack-bundle) command. The repository fixture exposes convenience scripts that invoke those commands and prepare the publication tree:

npm run repack:build:production:ios
npm run repack:build:production:android

Publish separate ios/ and android/ trees. Each tree contains its own mf-manifest.json, container/exposure scripts, nested lazy chunks, source maps, density images and fonts. Point each host build at the matching platform manifest; never serve an iOS widget graph to Android or vice versa. Host and widget EXPO_PUBLIC_* values are compiled from their respective projects, so do not use widget build-time variables as host runtime configuration.

Redeploying those remote artifacts is not Expo Updates or OTA. It does not replace the host launch bundle or its packaged local chunks. Host JavaScript, integration, dependency and native changes still require a new native build; only MF v2 widget artifacts follow Re.Pack's remote manifest lifecycle.

Production remotes must use HTTPS. The fixture's Android cleartext exception is limited to localhost and must not be copied for a public host. Optional storage, update policy, retry, runtime plugins and code verification remain ordinary Re.Pack configuration: use the existing ScriptManager storage and locator APIs plus CodeSigningPlugin when required. ExpoPlugin adds no parallel cache, signature, invalidation or security policy, and it does not require an Expo-specific setup entry.

Public environment variables

ExpoPlugin loads Expo-compatible dotenv files and inlines only static process.env.EXPO_PUBLIC_* accesses in application modules. Shell values take priority, followed by .env.<mode>.local, .env.local, .env.<mode> and .env. Missing files are optional. Private values can participate in dotenv expansion but are never passed to Babel or Rspack definitions.

Dot and string-literal bracket access are supported, including optional chains. Computed variables, destructuring, assignments, private keys and accesses in dependencies are left unchanged. EXPO_NO_DOTENV=1 disables dotenv files while retaining shell-provided public values. In production, EXPO_NO_CLIENT_ENV_VARS=1 disables public client injection entirely.

Changing or creating a selected dotenv file invalidates affected modules in development. Changes to the launching shell environment require restarting the Re.Pack development server.

Migrating from Expo Metro

  1. Create a migration branch and ensure the application has a static Expo config.
  2. Run npx @callstack/repack-expo init --dry-run and review the managed-file changes.
  3. Run init, execute its printed dependency command and run npx expo prebuild --clean.
  4. Run npx @callstack/repack-expo doctor.
  5. Replace expo start with repack:start; launch native clients through the generated repack:ios and repack:android scripts.
  6. Exercise development, Fast Refresh and both native Release builds before removing an existing production path.

init does not delete metro.config.js; Re.Pack simply does not read it. Keep the file only if another unsupported Expo/Metro workflow still needs it. This migration does not automatically enable federation and does not provide Expo CLI bundler parity, OTA parity, Metro Module Federation, MF v1 or raw enhanced plugin support. Add the application-owned MF v2 configuration above only after the non-federated Re.Pack path is working.

Troubleshooting

Start with the read-only doctor:

npx @callstack/repack-expo doctor
npx @callstack/repack-expo doctor --json

| Symptom or diagnostic | Recovery | | --- | --- | | DYNAMIC_EXPO_CONFIG | Use static app.json/package.json#expo, or apply the reported configuration manually before prebuild. The CLI will not execute application config code. | | RSPACK_CONFIG_CONFLICT | Preserve and update the existing config, or explicitly allow replacement with init --force. Keep only one ExpoPlugin and no separate RepackPlugin. | | New Architecture or Hermes conflict | Remove newArchEnabled: false or a non-Hermes jsEngine override. SDK 56+ supplies the supported defaults when those fields are absent. | | Expo Updates conflict | Set expo.updates.enabled to false and regenerate the native projects. OTA is not supported. | | Development client requests Metro or cannot find index.bundle | Rerun prebuild, start repack:start, launch with repack:ios/repack:android, and verify the generated Re.Pack commands with doctor. | | ScriptManager autolinking cannot be verified | Install dependencies, run a clean prebuild, install iOS pods when applicable, then rerun doctor. Do not add another native ScriptManager module. | | A Release lazy import fails | Confirm the chunk has a flat unique filename, rebuild the native application and inspect the underlying Rspack chunk-loader diagnostic. | | An MF v2 widget fails to load | Preserve the original MF/ScriptManager error in a host-owned boundary, verify the platform manifest and remote artifact URLs, restore the remote, then start a new loadRemote() attempt. | | An Android production widget works only over HTTP | Deploy the manifest, scripts and assets over HTTPS. The generated application does not allow arbitrary cleartext hosts. | | An Android packaged font cannot load | Clean and rebuild with the current ExpoPlugin; do not add an application-owned font-copy workaround. | | EAS attempts Metro/export:embed | Set EAS_BUILD_DISABLE_BUNDLE_JAVASCRIPT_STEP=1 on the selected EAS profile and rebuild. |

Support matrix

| Capability | V1 status | | --- | --- | | Expo SDK 56+ | Supported; template shapes are accepted when all required native seams validate. | | iOS and Android native applications | Supported. | | Rspack | Supported. | | Webpack | Unsupported. | | New Architecture and Hermes | Required; SDK 56+ defaults do not need explicit app-config fields. | | Expo Router | Supported. | | Resolvable plain package.json#main entry | Supported. | | Expo prebuild/CNG | Supported and required. | | Direct native development and Fast Refresh | Supported through Re.Pack and expo run:* --no-bundler. | | Local native Release and EAS Build | Supported. | | Expo Modules, images, density variants, fonts and source maps | Supported. | | Expo-compatible EXPO_PUBLIC_* environment variables | Supported. | | Monorepo and pnpm workspace resolution | Supported. | | Packaged local chunks through ScriptManager | Supported. | | Re.Pack Module Federation v2 | Supported as explicit application-owned configuration. | | Expo host -> Expo widget | Supported and validated on iOS and Android. | | Expo host -> ordinary Re.Pack MF v2 widget | Supported and validated. | | Production MF v2 widget artifacts | Supported as platform-specific remote deployments. | | Ordinary Re.Pack host -> Expo widget | Unsupported; the Expo native contract is not guaranteed. | | Module Federation v1 or raw @module-federation/enhanced/rspack | Unsupported. | | Metro Module Federation or @module-federation/metro | Unsupported. | | Generic remote chunks outside MF v2 | Unsupported by the Expo integration contract. | | Expo Go | Unsupported. | | expo-dev-client launcher, QR discovery and development manifests | Unsupported. | | expo start or expo export as Re.Pack commands | Unsupported. | | Expo Updates, EAS Update and OTA | Unsupported. | | Metro bundler integration | Unsupported. | | Old Architecture or JSC | Unsupported. | | Web, SSR, React Server Components and DOM Components | Unsupported. | | Executable app.config.* mutation | Unsupported; static Expo config is required by init. |