loading...

Developing NPM modules for Web and React Native simultaneously with Webpack

kylessg profile image Kyle Johnson ・5 min read

During my day to day as a developer, I work on a several projects that include both a web application and a React Native mobile app.

The latest project I've been working on is https://bullet-train.io which I've written a few posts about recently. This project, in particular, had a requirement to include a JS and React Native client libraries in order for frontend applications to use the service.

This post goes through my approach on how I structured my library in a way I can deploy regular updates to both modules simultaneously whilst updating their separate example applications examples to include the latest bundles. It also provides a link to the real example for you to check out.

What is Webpack?

This post does assume a moderate understanding of what Webpack is and the role it plays in your project. At a high-level Webpack takes entry files (e.g. index.js, screen.scss and other assets), analyses their dependences and bundles them together, transpiling the input when it needs to (e.g. converting es6 to vanilla JS with babel, scss to css with node-sass) to create a single output file.

In our case we're using it to create 2 javascript libraries to be published to NPM, we also create a copy of each library and deploy it to example applications for people to try.

The project

The client SDKs in my example act a user-friendly proxy to the Bullet Train REST API, it helps retrieve a list of feature flags / remote config based on an Environment Key. It also does a few a few things under the hood such as caching results with AsyncStorage and adds functions to tell me if a feature is enabled and what values they have configured.

Step 1: Identifying the shared code

Quite often when developing in React Native you could most likely make do with having just one JavaScript module that accomplishes what you wanted. However, there are some use cases where separate implementations have to work slightly differently or maybe include Native Bridges to access core device functionality.

In our case, the modules were very similar apart but needed to use separate implementations of fetch and Async Storage to function. To maximise reuse, the code was split into two entry files which provided platform specific polyfills to bullet-train-core.js.

Step 2: Creating a sensible project structure

A good place to start is to set out a suitable project structure. The project is split into 3 sections:

/

At the top level is our Webpack configuration and our library js, these files don't get included in any of our NPM modules directly but are used to generate each respective bundles. Since the aim is to manage 2 separate NPM modules, each has their own index entry file.

bullet-train-client

This folder contains our bundled web module and a simple example web application.

react-native-bullet-train

This folder contains our bundled React Native module and a simple example React Native application.

Step 3: Creating a development flow

To keep development easy, editing any of the top level files will kick off a few things:

  • 1. Trigger a minified bundle from index.js and bundle a minified output to /bullet-train-client/lib/index.js.
  • 2. Trigger a minified bundle from index.react-native.js and bundle a minified output to /react-native-bullet-train/lib/index.js.

As well as doing this we also want to deploy a non-minified version to each respective example folder so that we can debug it as we're testing.

To achieve this running node_modules/.bin/webpack --watch is the first step, it listens for any file changes and runs our Webpack build. Adding the above snippet to our top-level package.json so that this could be done by running npm run dev, the real example of this can be found this can be found here.

Step 4: Writing the Webpack config

At this point I had Webpack listening for changes, we just need to write the Webpack config file.

Our build system will be a little different from a standard website, where we'd normally have an entry file/output we actually have 4.


const defaultConfig = { //our base config
mode: "production",
devtool: 'source-map',
module: {
rules: [
{
test: /.(js)$/,
exclude: /node_modules/,
use: ['babel-loader']
}
]
}
};

...

module.exports = [ //export each bundle
webBundle, webExampleBundle, reactNativeBundle, reactNativeExampleBundle
];

This is the base config that we'll use for each one of our 4 bundles, it'll transpile any js file using babel. We set the mode to production so that the output gets minified and devtool to source-map so that we can see a readable version of the code when debugging.

The web bundle


const webBundle = Object.assign({}, defaultConfig, { //Bundle 1: compile the web client
output: {
filename: "index.js",
library: "bullet-train",
libraryTarget: "umd",
path: path.join(__dirname, '/bullet-train-client/lib'),
},
entry: {
main: './index.js'
}
});

Based on our base config, the web bundle creates a minified bundle to /bullet-train-client/lib/index.js. Setting the libraryTarget as umd is important as it tells webpack to make the output a JavaScript module so that we can do require('bullet-train-client') in our applications. The webExampleBundle is exactly the same as this configuration only that it outputs a file to /bullet-train-client/example/src.

The React Native bundle


const reactNativeBundle = Object.assign({}, defaultConfig, { //Bundle 3: compile the react native client
entry: {
main: './index.react-native.js'
},
externals: {
'react-native': 'react-native'
},
output: {
filename: "bullet-train.js",
library: "bullet-train",
libraryTarget: "umd",
path: path.join(__dirname, '/react-native-bullet-train/example'),
}
});

Unlike the web module, the React Native library needs to assume that React Native is installed as a peer dependency. This is where externals are used, externals are a way to exclude dependencies from a bundle and assume it already exists. If you didn't do this Webpack would fail to compile when evaluating require('react-native').

You'll need to use a configuration like this whenever your modules are coupled to external modules (e.g. open sourcing a react web component).

Step 5: Deploying

The next step was to write a simple way to deploy both of the client libraries and examples. This was as simple as writing the following npm script:

"deploy": "npm run build && cd ./bullet-train-client/ && npm publish && cd ../react-native-bullet-train && npm publish"

My process then is to just increment the NPM version in each package.json and run npm run deploy to publish both updated modules/example projects to NPM.

If you prefer learning by looking at code, all of it is open sourced on GitHub. Feel free to post any questions you have here!

Posted on by:

kylessg profile

Kyle Johnson

@kylessg

I drink coffee and make things.

Discussion

markdown guide
 

Thanks for reminding about "externals"! It should be written somewhere in big block letters.