Search code examples
javascriptreact-nativemetro-bundler

How to get React Native variant in Metro?


I am developing React Native application that includes different configurations for different possible clients, in a file such as src/config/config.js. These configurations are quite complex. The file is structured based on the client name as key, and the values as the object entries, e.g.:

export default {
  fooClient: {
    apiUrl: "https://foo.example.com/",
  barClient: {
    apiUrl: "https://bar.example.com/"
  }
}

Of course, there are many other option keys.

When building the app, I know for which client I want to do this, by specifying an Android build variant, e.g.:

ENVFILE=.env npx react-native run-android --variant fooDebug --appIdSuffix foo

For security reasons, I don't want keys of other clients to be included in the config file though. What are my options to remove all other client configs from this file before I build the app and ship it to a client?

I thought about the following: I modify the packager so that it strips out the keys that do not correspond to the current build variant.

I now have a transformer plugin for Metro that does the following:

const upstreamTransformer = require('metro-react-native-babel-transformer');

module.exports.transform = function(src, filename, options) {
  if (typeof src === 'object') {
    // handle RN >= 0.46
    ({ src, filename, options } = src);
  }

  if (filename.endsWith('config.js')) {
    console.log('Transforming ' + filename);
    let srcStripped = src.replace(';', '').replace('export default ', '');
    let configObj = JSON.parse(srcStripped);
    // TODO: get the build variant and strip all keys that we do not need from configObj
    return upstreamTransformer.transform({
      src: 'export default ' + JSON.stringify(configObj) + ';',
      filename: filename,
      options
    });
  } else {
    return upstreamTransformer.transform({ src, filename, options });
  }
};

But how do I know which build variant is being used?

If this seems like an XY problem, I am happy to explore alternatives to building the configuration dynamically. I cannot, however, use environment variables, since the configuration will be too complex for it to be just a list of .env keys.


Solution

  • You shouldn't use Metro transform this way. It's not clean and it may lead to missing configuration and/or damaged syntax sooner or later.

    What I have done and suggest you, is to create 3 different configuration files under src/config/; one file for fooClient.js, one file for barClient.js and one last file with common configuration client.js. All files will export default configuration objects, but inside each fooClient and barClient, you'll use deepmerge module to merge client.js config:

    client.js:

    export default {
      commonSettingA: "...",
      commonSettings: {
        ...
      }
      ...
    }
    

    fooClient.js:

    import merge from 'deepmerge';
    import config from './config';
    
    export default merge.all([
      config,
      {
        apiUrl: "https://foo.example.com/",
      }
    ]);
    

    barClient.js:

    import merge from 'deepmerge';
    import config from './config';
    
    export default merge.all([
      config,
      {
        apiUrl: "https://bar.example.com/",
      }
    ]);
    

    Then you can use an environment variable to pass the needed configuration and create a propriate metro resolve; @react-native-community/cli does not pass command line arguments to metro config script. You can use process.argv to parse it by yourself, but it's not worth it.

    Here is how you can create a resolve inside metro.config.js using environment variable:

    const path = require("path");
    
    module.exports = {
      projectRoot: path.resolve(__dirname),
    
      resolver: {
        sourceExts: ['js', 'jsx', 'ts', 'tsx'],
        extraNodeModules: {
          // Local aliases
          "@config": path.resolve(__dirname, "src/config", `${process.env.METRO_VARIANT}Client.js`)
        }
      }
    };
    

    Using this resolve, you'll have to import the configuration like this:

    import config from '@config';
    

    Then you add 2 package.json scripts, one for fooClient and one for barClient:

    {
       ...
      "scripts": {
        "run:android:foo": "METRO_VARIANT=foo ENVFILE=.env npx react-native run-android --variant fooDebug --appIdSuffix foo",
        "run:android:bar": "METRO_VARIANT=bar ENVFILE=.env npx react-native run-android --variant barDebug --appIdSuffix bar",
        ...
      }
      ...
    }
    

    Then you just run the needed script:

    yarn run:android:foo # will build with fooClient.js
    
    yarn run:android:bar # will build with barClient.js