metro: Asset paths (httpServerLocation) invalid when asset is in watchFolders

Do you want to request a feature or report a bug? Bug

What is the current behavior? When I specify a directory in watchFolders from outside of my project root, the path reported via httpServerLocation is broken. For example, /symlinked-watch-folder/subdir/image@3x.png.

If the current behavior is a bug, please provide the steps to reproduce and a minimal repository on GitHub that we can yarn install and yarn test.

  1. Create a React Native project in one directory.
  2. Create a sibling directory called symlinked-watch-folder with a Node module in it named symlinked-module.
  3. Add assets to this module, specifically one in subdir/image@3x.png.
  4. In this module, reference the image like so: <Image source={require('../subdir/image.png')} />
  5. Now from the RN project directory, use npm to install the module: npm install ../symlinked-watch-folder.
  6. Make a component in your project root that displays the module’s component with the image in it.
  7. Run the React Native project. The image will not appear and a request for the image will 404 because the path is invalid.

What is the expected behavior? It looks like there are different valid locations, including: /assets/symlinked-module/subdir/image@3x.png /assets/subdir/image@3x.png /subdir/image@3x.png

More info Because this repo isn’t very publicly documented, I can’t tell exactly what’s going on. But it looks like getAssetData is trying to path.join these two paths: /assets ../symlinked-watch-folder/subdir/image@3x.png https://github.com/facebook/metro/blob/bba48f068c6e709187d36170d3e5d49b7e9f6169/packages/metro/src/Assets.js#L181 The ../ wipes out the /assets and we end up with a result that isn’t valid: /symlinked-watch-folder/subdir/image@3x.png

I believe the relative path is coming from here: https://github.com/facebook/metro/blob/bba48f068c6e709187d36170d3e5d49b7e9f6169/packages/metro/src/DeltaBundler/Serializers/getAssets.js#L44

The asset is being served correctly, it’s just that the httpServerLocation that’s being generated is not correct. I can access the asset just fine if I correct the path to one of these: /assets/symlinked-module/subdir/image@3x.png /assets/subdir/image@3x.png /subdir/image@3x.png

If there is some way to handle this using the documented options, please let me know. (cc: @rafeca )

Please provide your exact Metro configuration and mention your Metro, node, yarn/npm version and operating system. I’ve tried both Metro 0.47.1 and Metro 0.48.0 Node: 10.11.0 npm: 6.4.1 OS: macOS High Sierra

About this issue

  • Original URL
  • State: open
  • Created 6 years ago
  • Reactions: 34
  • Comments: 22 (1 by maintainers)

Commits related to this issue

Most upvoted comments

FYI, I abstracted @vitramir fix in a module that I’m using in a monorepo: get-metro-android-assets-resolution-fix.js.

You can use it this way:

const {
  getMetroAndroidAssetsResolutionFix,
} = require("@rnup/build-tools");

const androidAssetsResolutionFix = getMetroAndroidAssetsResolutionFix({
  depth: 3,
});

module.exports = {
  transformer: {
    // Apply the Android assets resolution fix to the public path...
    publicPath: androidAssetsResolutionFix.publicPath,
  },
  server: {
    // ...and to the server middleware.
    enhanceMiddleware: (middleware) => {
      return androidAssetsResolutionFix.applyMiddleware(middleware);
    },
  }
};

Wondering if we could just default to a huge depth (e.g. 20) without drawbacks.

Edit: I also published it in react-native-monorepo-tools.

I got this issue while using yarn workspaces and here is how I solved it:

1)Our first issue is resources destination path outside of /assets directory. It breaks both debug server and bundle.

There is config transformer.publicPath which default value is /assets. So, I changed it to /assets/dir1/dir2/dir3 (because my watchDir is ‘…/…/…/’). Now it will generate valid destination path for resources outside of projectRoot. For example /assets/dir1/dir2/dir3/../../common/images/image.png. react-native bundle will work too.

2)The second issue with Metro Server. It should resolve requests for IOS (http://localhost:8081/assets/dir1/dir2/dir3/../../common/images/image.png) and Android (http://localhost:8081/assets/dir1/common/images/image.png) the same way.

I changed server.enhanceMiddleware config this way:

server: {
    enhanceMiddleware: (middleware, server) => {
      return (req, res, next) => {
        if(req.url.startsWith("/assets/dir1/dir2/dir3")){
          req.url = req.url.replace('/assets/dir1/dir2/dir3', '/assets');
        }else if(req.url.startsWith("/assets/dir1/dir2")){
          req.url = req.url.replace('/assets/dir1/dir2', '/assets/..');
        }else if(req.url.startsWith("/assets/dir1")){
          req.url = req.url.replace('/assets/dir1', '/assets/../..');
        }else if(req.url.startsWith("/assets")){
          req.url = req.url.replace('/assets', '/assets/../../..');
        }
        return middleware(req, res, next);
      };
    },
  },

I want to notice here two more things:

1)This code doesn’t use config value transformer.publicPath. I think it is wrong. https://github.com/facebook/metro/blob/d9c556c04dc863b334720ab7eed9c94bc2841995/packages/metro/src/Server.js#L306-L307

2)And another solution of this issue is to update code of assetUrlPath generation and replace ../ into something like __/ or _.._/.

For example, this line https://github.com/facebook/metro/blob/d9c556c04dc863b334720ab7eed9c94bc2841995/packages/metro/src/Assets.js#L197-L199 may become:

let assetUrlPath =path.join(publicPath, path.dirname(localPath).replace(/..\//g, '__/'));

Also we should add reverse operation at Server module. I can make PR with this changes if community approve the idea.


I am currently using this config and it works with yarn workspaces as expected:

module.exports = {
  watchFolders: [path.resolve(__dirname, '../../../')],
  server: {
    enhanceMiddleware: (middleware, server) => {
      return (req, res, next) => {
        if(req.url.startsWith("/assets/dir1/dir2/dir3")){
          req.url = req.url.replace('/assets/dir1/dir2/dir3', '/assets');
        }else if(req.url.startsWith("/assets/dir1/dir2")){
          req.url = req.url.replace('/assets/dir1/dir2', '/assets/..');
        }else if(req.url.startsWith("/assets/dir1")){
          req.url = req.url.replace('/assets/dir1', '/assets/../..');
        }else if(req.url.startsWith("/assets")){
          req.url = req.url.replace('/assets', '/assets/../../..');
        }
        return middleware(req, res, next);
      };
    },
  },
  transformer: {
    publicPath: '/assets/dir1/dir2/dir3',
    babelTransformerPath: path.resolve(
      __dirname,
      './metro-transformer/index.js'
    ),
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false,
      },
    }),
  },
};

I don’t provide babelTransformer. Nothing interesting there.

I created a simple repro for this using yarn workspaces: https://github.com/brentvatne/metro-issue-290

we’re running into this in the expo monorepo, only solution for us for now is to use nohoist :\

perhaps the path should be given a uri param? eg: http://localhost:1234/assets?path=../lol/whatever.png&platform=android&so-on&so-forth

I’m facing the same problem. I can’t display local images if they are on my watchFolders. I’m not familiar with metro but when I bundle my app it works, so I guess there is a problem in my metro config…

Here is my project stucture :

- native
- web
- shared (here are my assets)

And here is my metro config, present in the native folder :

module.exports = {
  getTransformModulePath() {
    return require.resolve("react-native-typescript-transformer");
  },
  getSourceExts() {
    return ["ts", "tsx"];
  },
  resolver: {
    extraNodeModules: {
      react: path.resolve(__dirname, "node_modules/react"),
      "react-native": path.resolve(__dirname, "node_modules/react-native")
    }
  },
  projectRoot: path.resolve(__dirname),
  watchFolders: [path.resolve(__dirname, "../shared")]
};

Do you see where is my problem here ? 😕 (I can import shared components, it only failed with assets)

React-Native : 0.57.4 Metro : 0.48.5

Thanks @vitramir & @mmazzarolo 🙏

FYI @mmazzarolo the link in your answer results in 404, it needs to be updated to this 😃

@papigers Actually, a nicer solution is to use an asset plugin.

I created metro-asset-plugin.js:

module.exports = function(assetData) {
	if (assetData.httpServerLocation.indexOf('../') !== -1) {
		assetData.httpServerLocation = assetData.httpServerLocation.replace(/\.\.\//, '_')
	}
	return assetData
}

Then in metro.config.js I include it thus:

...
transformer: {
    assetPlugins: [require.resolve('./metro-asset-plugin')],
},

I’m having the same issue: iOS not showing watchFolder images only on release build - debug build works fine.

I bumped into this problem too! Currently as a workaround I did what suggested in #322.

Also, for Androd I need the patch only in dev mode (using metro server) while for iOS I have to do the opposite (when I build the bundle). 😕

React-Native: 0.59.0 Metro: 0.51.1

Yep, I think changing the asset path to be given as a query param is the only way forward here.

Brief recap for others:

  • In dev mode, assets are requested from the Metro server using a path sent to the server, so something like http://localhost:8081/assets/images/foo.png
  • When watchFolders is used in Metro config, as is used in multi-repo or mono-repo projects, relative paths are used for assets (e.g. ../../../common/assets/images/image.png) These may be way outside of the root.
  • In iOS, the code sending the request off sends the path exactly as it gets it. In this example, that would be http://localhost:8081/assets/../../../common/assets/images/image.png. The Metro server handles it fine.
  • In Android, which uses okhttp to send requests, relative paths are stripped (per the URI spec section 5.2.4) before the request is made. That means the request ends up being http://localhost:8081/images/image.png which is invalid.

Unless there’s some way to configure okhttp to stop removing dot segments, the only way forward (and the more sane approach) would be to send asset paths off as query params.

More info was posted in https://github.com/facebook/metro/issues/322#issuecomment-445642199 and a fix was supposed to be in https://github.com/facebook/metro/commit/0eaa741827bc3397ca7368105d62b1fa45fbbbe7 but looks like that didn’t fully solve it. Unfortunately, the author @rafeca left Facebook earlier this year, so probably won’t get much more attention from him.

Hi, I also encountered this issue when using symlink to resolve asset. Im using metro-with-symlinks to resolve symlink packages.

Here’s the repo to reproduce the issue: https://github.com/adrianha/metro-symlink - Works fine react-native: 0.57.1

https://github.com/adrianha/metro-symlink/tree/0.57.2 - Assets path incorrect react-native: 0.57.2

If you have a monorepo setup, and are experiencing issues resolving assets in node_modules on iOS release builds after updating to React Native 0.62, it is likely that you are experiencing https://github.com/react-native-community/upgrade-support/issues/26. The easiest solution is to patch react-native with https://github.com/facebook/react-native/commit/7deeec73966d84140492c2a767819977318c4d2d.