Generally, is there a way to customize the app code during build step?
Specifically, there is the typical need to make requests from the application to the local backend ([1], [2], [3], [4]).
localhost
doesn't work since the server and the app are on different hosts (when using Android emulator or an actual physical device).With Webpack a case like that could be solved with DefinePlugin replacing a placeholder with the IP address of the machine the build is happening on.
We ended up using somewhat hacky approach inspired by transformers like react-native-typescript-transformer
or react-native-sass-transformer
. It's idea is pretty much equivalent to the mentioned DefinePlugin
of Webpack.
First, some transformer files in the project directory (you can name them however you like, just update the references):
configBuildReplacements.js
// whatever logic you need
module.exports = {
API_HOST_PLACEHOLDER: `http://${getLocalNetworkAddress()}:3000`,
SOME_OTHER_DYNAMIC_VALUE: someFun(),
}
configBuildReplaceTransformer.js
const semver = require('semver')
let upstreamTransformer = null
const reactNativeVersionString = require('react-native/package.json').version
const reactNativeMinorVersion = semver(reactNativeVersionString).minor
if (reactNativeMinorVersion >= 56) {
upstreamTransformer = require('metro/src/reactNativeTransformer')
}
else if (reactNativeMinorVersion >= 52) {
upstreamTransformer = require('metro/src/transformer')
}
else if (reactNativeMinorVersion >= 47) {
upstreamTransformer = require('metro-bundler/src/transformer')
}
else if (reactNativeMinorVersion === 46) {
upstreamTransformer = require('metro-bundler/build/transformer')
}
else {
// handle RN <= 0.45
const oldUpstreamTransformer = require('react-native/packager/transformer')
upstreamTransformer = {
transform({ src, filename, options }) {
return oldUpstreamTransformer.transform(src, filename, options)
},
}
}
module.exports.transform = function (src, filename, options) {
// handle RN >= 0.46
if (typeof src === 'object') {
({ src, filename, options } = src)
}
const replacements = require('./configBuildReplacements')
const modifiedSrc = Object.keys(replacements).reduce(
(src, replacementKey) => src.replace(
new RegExp(replacementKey, 'g'),
replacements[replacementKey],
),
src,
)
return upstreamTransformer.transform({
src: modifiedSrc,
filename,
options,
})
}
The exported transform
function uses the exported object from the previous file configBuildReplacements.js
as a dictionary to replace key substrings with value substrings in the source code before handing this code to the default (upstream) transformer.
And to connect this new transformer to the project:
with Expo, add the transformer
packager option to app.json
:
{
"expo": {
"packagerOpts": {
"transformer": "configBuildReplaceTransformer.js"
}
}
}
without Expo, add getTransformModulePath()
to rn-cli.config.js
(which is the default path to the optional config file for React Native CLI, which is extremely poorly documented at the moment of this writing):
module.exports = {
getTransformModulePath() {
return require.resolve('./configBuildReplaceTransformer')
},
}
After this is done, just like with DefinePlugin
, code like
get('API_HOST_PLACEHOLDER/info')
will become something like
get('http://192.168.42.23:3000/info')