Static images fail to load in monorepo on iOS
Hi Metro team 👋
I’m opening this new issue because I’m still experiencing the same bug as in #1599, but the original reporter is not active anymore and comments are closed.
Summary
In a monorepo (Yarn workspaces), iOS fails to load static images imported via require(...) when the image file is located outside the app directory.
The behavior is identical to #1599:
- The image silently fails to display on iOS
Hi @jiroscripts. In #1599 the issue was a misplaced metro config. If you’re experiencing a different issue, we’ll need more details and reproduction to investigate.
Hey @robhogan
I tried to place the metro config at the root of my monorepo without any success sadly
I share you some file here to help to debug:
apps/mobile/package.json
{
"name": "@bleulibellule/mobile",
"version": "2.0.5",
"private": true,
"reactNativePermissionsIOS": [
"Notifications",
"AppTrackingTransparency",
"LocationWhenInUse",
"LocationAlways"
],
"scripts": {
"start": "react-native start"
},
"dependencies": {
"@batch.com/react-native-plugin": "^11.1.0",
"@bleulibellule/algolia-analytics-logger": "workspace:^",
"@bleulibellule/analytics-logger": "workspace:^",
"@bleulibellule/api": "workspace:^",
"@bleulibellule/assets": "workspace:^",
"@bleulibellule/deeplink-builder": "workspace:^",
"@bleulibellule/nominatim-api": "workspace:^",
"@bleulibellule/react-native-alma-widgets": "workspace:^",
"@bleulibellule/storyblok-renderer": "workspace:^",
"@bleulibellule/themes": "workspace:^",
"@bleulibellule/ui": "workspace:^",
"@didomi/react-native": "^2.18.0",
"@invertase/react-native-apple-authentication": "^2.4.1",
"@jiroscripts/react-native-device-brightness": "^0.2.0",
"@lyracom/react-native-sdk-payment-module": "^3.0.0",
"@mainbleulibellule/react-native-df-ui-kit": "^1.0.0-beta.6",
"@maplibre/maplibre-react-native": "^10.4.0",
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-clipboard/clipboard": "^1.16.3",
"@react-native-community/blur": "^4.4.1",
"@react-native-community/datetimepicker": "^8.5.0",
"@react-native-community/geolocation": "^3.4.0",
"@react-native-documents/picker": "^11.0.0",
"@react-native-firebase/analytics": "^23.5.0",
"@react-native-firebase/app": "^23.5.0",
"@react-native-firebase/crashlytics": "^23.5.0",
"@react-native-firebase/remote-config": "^23.5.0",
"@react-native-google-signin/google-signin": "^16.0.0",
"@react-native-masked-view/masked-view": "^0.3.2",
"@react-native-picker/picker": "^2.11.4",
"@react-native-vector-icons/feather": "^12.3.0",
"@react-native-vector-icons/material-icons": "^12.3.0",
"@react-native/virtualized-lists": "^0.81.4",
"@react-navigation/bottom-tabs": "^7.7.2",
"@react-navigation/core": "^7.13.0",
"@react-navigation/elements": "https://github.com/jiroscripts/react-navigation/archive/refs/tags/react-navigation-elements-v2.8.0-gitpkg.tar.gz",
"@react-navigation/material-top-tabs": "^7.4.1",
"@react-navigation/native": "^7.1.19",
"@react-navigation/native-stack": "^7.6.1",
"@react-navigation/stack": "^7.6.1",
"@rnx-kit/metro-config": "^2.2.0",
"@rnx-kit/metro-resolver-symlinks": "^0.2.7",
"@rnx-kit/tools-node": "^3.0.2",
"@rnx-kit/tools-react-native": "^2.3.1",
"@storybook/addon-essentials": "^8.2.2",
"@storybook/addon-interactions": "^8.2.2",
"@storybook/addon-links": "^8.2.2",
"@storybook/addon-ondevice-actions": "^8.2.0-alpha.1",
"@storybook/addon-ondevice-backgrounds": "^8.2.0-alpha.1",
"@storybook/addon-ondevice-controls": "^8.2.0-alpha.1",
"@storybook/addon-ondevice-notes": "^8.2.0-alpha.1",
"@storybook/addon-react-native-server": "^0.0.5",
"@storybook/addon-react-native-web": "^0.0.22",
"@storybook/blocks": "^8.2.2",
"@storybook/builder-webpack5": "^8.2.2",
"@storybook/core-common": "^8",
"@storybook/docs-tools": "^8",
"@storybook/global": "^5.0.0",
"@storybook/react": "^8.2.2",
"@storybook/react-native": "^8.2.0-alpha.1",
"@storybook/react-native-theming": "^8.2.0-alpha.1",
"@storybook/react-webpack5": "^8.2.2",
"@storybook/test": "^8.2.2",
"@tanstack/query-core": "^5.90.8",
"@tanstack/react-query": "^5.90.5",
"@turf/bbox": "^7.2.0",
"@turf/distance": "^7.2.0",
"@turf/helpers": "^7.2.0",
"algoliasearch": "^5.42.0",
"axios": "^1.13.1",
"base-64": "^1.0.0",
"dayjs": "^1.11.18",
"deepmerge": "^4.3.1",
"formik": "^2.4.6",
"geojson": "^0.5.0",
"i18next": "^25.6.0",
"instantsearch.js": "^4.81.0",
"jotai": "^2.15.0",
"jotai-tanstack-query": "^0.11.0",
"jsbarcode": "^3.12.1",
"lodash.debounce": "^4.0.8",
"luxon": "^3.7.2",
"moment": "^2.30.1",
"openapi-fetch": "^0.15.0",
"react": "19.1.0",
"react-i18next": "^16.2.2",
"react-instantsearch-core": "^7.17.0",
"react-native": "0.81.4",
"react-native-blob-util": "^0.23.1",
"react-native-config": "^1.5.9",
"react-native-credentials-manager": "^0.8.1",
"react-native-device-info": "^14.1.1",
"react-native-edge-to-edge": "^1.7.0",
"react-native-fbsdk-next": "^13.4.1",
"react-native-gesture-handler": "^2.29.0",
"react-native-hole-view": "patch:react-native-hole-view@npm%3A3.0.1#~/.yarn/patches/react-native-hole-view-npm-3.0.1-c00ea6c202.patch",
"react-native-image-picker": "^8.2.1",
"react-native-image-resizer": "^1.4.5",
"react-native-keyboard-aware-scroll-view": "^0.9.5",
"react-native-maps": "^1.26.18",
"react-native-mmkv": "^4.0.0",
"react-native-monorepo-tools": "^1.2.1",
"react-native-network-logger": "^2.0.1",
"react-native-nitro-modules": "^0.31.3",
"react-native-pager-view": "^6.9.1",
"react-native-permissions": "^5.4.3",
"react-native-phone-input": "^1.3.7",
"react-native-picker-select": "^9.3.1",
"react-native-reanimated": "^4.1.3",
"react-native-reanimated-carousel": "^4.0.3",
"react-native-render-html": "patch:react-native-render-html@npm%3A6.3.4#~/.yarn/patches/react-native-render-html-npm-6.3.4-35624feabf.patch",
"react-native-safe-area-context": "^5.5.2",
"react-native-screenguard": "^1.1.0",
"react-native-screens": "^4.18.0",
"react-native-size-matters": "^0.4.2",
"react-native-svg": "^15.14.0",
"react-native-tab-view": "^4.2.0",
"react-native-toast-message": "^2.3.3",
"react-native-turbo-image": "^1.23.1",
"react-native-unistyles": "^3.0.17",
"react-native-url-polyfill": "^3.0.0",
"react-native-video": "^7.0.0-alpha.7",
"react-native-vision-camera": "^4.7.2",
"react-native-volume-manager": "^2.0.8",
"react-native-webview": "^13.16.0",
"react-native-worklets": "^0.5.2",
"react-native-youtube-iframe": "^2.4.1",
"react-native-zendesk-messaging": "patch:react-native-zendesk-messaging@npm%3A0.3.2#~/.yarn/patches/react-native-zendesk-messaging-npm-0.3.2-7900bb146f.patch",
"rn-range-slider": "^2.2.2",
"search-insights": "^2.17.3",
"semver": "^7.7.3",
"string-strip-html": "^13.5.0",
"styled-components": "^6.1.19",
"uuid-random": "^1.3.2",
"warn-once": "^0.1.1",
"yup": "^1.7.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@nx/eslint-plugin": "^22.0.2",
"@nx/react-native": "^22.0.2",
"@react-native-community/cli": "20.0.0",
"@react-native-community/cli-platform-android": "20.0.0",
"@react-native-community/cli-platform-ios": "20.0.0",
"@react-native/babel-preset": "0.81.4",
"@react-native/eslint-config": "0.81.4",
"@react-native/metro-config": "0.81.4",
"@react-native/typescript-config": "0.81.4",
"@testing-library/react-native": "^13.3.3",
"@types/babel__core": "^7",
"@types/babel__preset-env": "^7",
"@types/base-64": "^1",
"@types/geojson": "^0",
"@types/jest": "^29.5.13",
"@types/lodash.debounce": "^4",
"@types/luxon": "^3",
"@types/node": "^24.9.2",
"@types/react": "^19.1.0",
"@types/react-test-renderer": "^19.1.0",
"@types/semver": "^7",
"babel-plugin-module-resolver": "^5.0.2",
"babel-plugin-transform-remove-console": "^6.9.4",
"eslint": "^8.19.0",
"jest": "^29.6.3",
"jsonc-eslint-parser": "^2.4.1",
"prettier": "2.8.8",
"react-native-svg-transformer": "^1.5.1"
}
}
package.json
{
"name": "@bleulibellule/source",
"version": "2.0.5",
"private": true,
"scripts": {
"test": "jest --coverage",
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx",
"jscpd": "jscpd src -o reports/jscpd -r html",
"cpx": "genese cpx src",
"quality": "npm run lint && npm run jscpd",
"sonar": "sonar-scanner && open http://localhost:9000/dashboard?id=BleuLibellule",
"upgrade-version": "./scripts/version-upgrader.sh"
},
"engines": {
"node": ">=20"
},
"packageManager": "[email protected]",
"lint-staged": {
"*.{js,jsx,ts,tsx,json}": [
"prettier --write",
"eslint --fix"
]
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"workspaces": {
"packages": [
"apps/*",
"libs/*"
]
},
"devDependencies": {
"@nx/eslint": "^22.0.2",
"@nx/eslint-plugin": "^22.0.2",
"@nx/jest": "^22.0.2",
"@nx/js": "22.0.2",
"@nx/next": "^22.0.2",
"@nx/react-native": "22.0.2",
"@nx/web": "22.0.2",
"@nx/workspace": "22.0.2",
"@rnx-kit/metro-config": "^2.2.0",
"@swc-node/register": "~1.9.1",
"@swc/core": "~1.5.7",
"@swc/helpers": "~0.5.11",
"@types/node": "20.19.9",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.6",
"@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2",
"eslint": "^9.38.0",
"jiti": "2.4.2",
"jsdom": "~22.1.0",
"jsonc-eslint-parser": "^2.4.1",
"lint-staged": "^16.2.6",
"nx": "22.0.2",
"swc-node": "^1.0.0",
"ts-node": "^10.9.2",
"tslib": "^2.3.0",
"typescript": "~5.9.2"
}
}
metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const { makeMetroConfig } = require('@rnx-kit/metro-config');
const path = require('path');
const defaultConfig = getDefaultConfig(__dirname);
const { assetExts, sourceExts } = defaultConfig.resolver;
const workspaceRoot = __dirname;
const projectRoot = path.resolve(workspaceRoot, 'apps/mobile');
/**
* Metro configuration
* https://reactnative.dev/docs/metro
*
* @type {import('@react-native/metro-config').MetroConfig}
*/
const customConfig = {
cacheVersion: '@bleulibellule/mobile',
projectRoot,
watchFolders: [workspaceRoot],
transformer: {
babelTransformerPath: require.resolve(
'react-native-svg-transformer/react-native'
),
},
resolver: {
assetExts: [...assetExts.filter((ext) => ext !== 'svg'), 'mp4'],
sourceExts: [...sourceExts, 'cjs', 'mjs', 'svg', 'ts', 'tsx'],
nodeModulesPaths: [
path.resolve(workspaceRoot, 'node_modules'),
path.resolve(projectRoot, 'node_modules'),
],
},
};
module.exports = mergeConfig(defaultConfig, customConfig);
App.tsx
import { Image } from 'react-native';
import backIconSource from '@react-navigation/elements/lib/module/assets/back-icon.png';
export const App = () => {
return (
<Image
source={backIconSource}
style={{ width: 100, height: 100, backgroundColor: 'red' }}
/>
);
};
export default App;
EDIT
The Console app reports these messages:
par défaut 15:13:13.276158+0100 BleuLibellule [C24 694EAF98-E997-4181-BF1C-33E5419385C0 Hostname#e16a780d:8081 tcp, url: http://localhost:8081/assets/../../libs/assets/src/lib/images/No_image_available.png, definite, attribution: developer] cancel
par défaut 15:13:13.276376+0100 BleuLibellule [C24 694EAF98-E997-4181-BF1C-33E5419385C0 Hostname#e16a780d:8081 tcp, url: http://localhost:8081/assets/../../libs/assets/src/lib/images/No_image_available.png, definite, attribution: developer] cancelled
[C24.1.1 6455B039-BBFC-44B5-A452-A7D2C31A9E04 ::1.49675<->::1.8081]
Connected Path: satisfied (Path is satisfied), viable, interface: lo0
Privacy Stance: Not Eligible
Duration: 30.626s, DNS @0.001s took 0.001s, TCP @0.003s took 0.000s
bytes in/out: 603/321, packets in/out: 2/1, rtt: 0.001s, retransmitted bytes: 0, out-of-order bytes: 0
ecn packets sent/acked/marked/lost: 0/0/0/0
In package.json
"resolutions": { "metro": "0.82.5", "metro-config": "0.82.5", "metro-core": "0.82.5" }
I've rolled back the version for now, and that helped me.
@dsshard You rolled back to which version ?
I found a metro config that works but you need to make some ajustements to make it works. The key is to define projectRoot at the root of the monorepo cause iOS do not suport uri with double dots (..) inside
It is actually an issue only in iOS in debug mode cause of the metro server assets paths