3

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.

slhck
  • 36,575
  • 28
  • 148
  • 201

2 Answers2

6

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
Christos Lytras
  • 36,310
  • 4
  • 80
  • 113
  • I'm coming back to this answer as I'm having trouble understanding how the `METRO_VARIANT` variable is actually passed to the Metro server when calling `run-android`. The Metro server should already be running at this point in time, started by `npx react-native start`, right? So how does it get the env from the `run-android` call from another shell, and decide based on that? – slhck Apr 07 '20 at 19:16
  • If you start metro server manually, then you'll have to set the environment variable to the `npx react-native start` script as well. Does this fail to pass the `METRO_VARIANT` if you leave it start metro automatically? – Christos Lytras Apr 07 '20 at 21:44
  • You were right. There was another bug masking the fact that the env was not picked up. Thanks again! – slhck Apr 15 '20 at 14:21
  • 1
    Is there a reason why passed ENVs on MacOS turn undefined in metro.config.js, but not in any other application file? When bundling on Windows, ENVs are passed correctly. I have tried passing them the way you did it / using the `cross-env` package / using `react-native-dotenv` package. The problem persists. – Alan Jereb Nov 02 '21 at 13:57
1

Can't you just add a "duplicated" parameter to the command?

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

And get it with process.env.METRO_VARIANT

You might not want to make your setup much more complex. Depending on the number of the number of different clients, you might wanna go with multiple schemas/variants per client, but that is probably way overkill.

Frexuz
  • 4,732
  • 2
  • 37
  • 54
  • I guess that's a quick and dirty workaround, thanks. I was hoping to learn more about how Metro is called from RN and if there could be some way to pass or access the variant. – slhck Feb 05 '20 at 20:02