How to create an NPM package out of a React app
In this post we’ll look at how to create an NPM package out of the build of a React application. Specifically, we’ll use the build output of a Create-React-App and create a package which exports the paths to the index.html
and build
folder. The enables the static files to be served from a backend such as express
. Even though this example uses React, I believe the technique should apply to all front-end frameworks as they all eventually generate HTML, JS, and CSS to be rendered by browsers.
You might be asking:
Why would you want to create a package?
Good question! It’s true in most cases your app is deployed online and viewed as a website so the user simply visits a known URL rather than downloading a package. In my case the app acted as an interface to communicate with a server running on the local machine over HTTP. The API between them needed to be known and changes coupled.
In normal websites, both the UI and Server are under your control so when you redeploy the interface to use a new feature which requires a new endpoint. You can also guarantee the server is updated to expose the new endpoint the feature needs. However, when the server is running on localhost (which is required for other reasons outside scope of post) you can’t guarantee this would be updated.
This requirement made the traditional practice of hosting the interface as a website undesirable. This would have required the user to know which version of the server they were running and then make sure they put the same version in the URL such as www.react-ui.com/admin/v2.20.1/home
and there could be many user errors due to version mismatch that would be hard to diagnose due only partial API support.
Instead eliminate that class of errors by serving the UI from this local server. This server knows the version of itself and it can request the correct UI using standard NPM package versions so they are guaranteed to be the same and remove the burden from customer. You could think of this being similar to an Electron app where it can have a UI and browser like features but also use lower level node features, but this is much lighter weigh solution and still uses a normal browser experience. This post will focus on the UI as that’s the unconventional part and the interesting part.
Now that we’re clear on why the package is needed. Let’s look at how to create the package.
What’s the goal?
First let’s look at what is needed in order to host an app on Express and compare with the output of CRA.
On the CRA website you can see how to server the output from express.
Note here the only information we need is the path to the /build folder to serve static files and path to index.html to route all requests.
Build Folder: path.join(__dirname, ‘build’)
Index File: path.join(__dirname, ‘build’, ‘index.html’)
Next, let’s look at how to get this information from within the package.
Create a new React app.
Start by creating a new react app
npx create-react-app cra-package --template typescript
If we go into the project cd cra-package
and then build the app npm run build
Note the /build
folder andindex.html
file referenced above.
Now we need to create a package of this build folder contents, but it also must export theses special paths and work regardless of where it’s installed on the local machine or in node_modules
.
First let’s write a script to create the package
folder.
In the root of the cra-package
create a new folder scripts
which will contain a script to generate the package folder.
Run the following commands
mkdir scripts
cd scripts
npx tsc --init
Adjust the tsconfig.json
to only compile createPackage.ts
which we’ll create now.
Create the createPackage.ts
with the following contents:
The Gist seems to be clipped by medium. Here is direct link
https://gist.github.com/mattmazzola/97c1b4fa2036f5b9ada78131e79e4bc1
Next let’s create that index.ts
script which this script depends on.
Similarly in the root of the cra-package
create a new folder packageScripts
to represent scripts that we’ll put inside the package. This is a separate folder because it has a separate tsconfig.json
settings.
Well use typescript again and leverage the compilation process to output into the package
folder.
mkdir packageScripts
cd ./packageScripts
npx tsx --init
Create an index.ts
file will represent the main script of our package.
Add these contents:
Notice we use __dirname
this is what makes the package work regardless of where it’s installed and not depend on hard-coded paths.
See: https://nodejs.org/api/modules.html#modules_dirname
Create and NPM script to run two typescript projects
Add an NPM script in the package.json to actually utilize these two scripts you just created and create the package
folder.
"builduipackage": "tsc -p ./packageScripts/tsconfig.json --outDir ./package && tsc -p ./scripts/tsconfig.json && node ./scripts/createPackage.js",
Reminder that the first compilation of packageScripts
which effectively copies the output to the package folder, then the execution of the createPackage.ts
script effectively copies the build
folder into the package
folder, and creates a new package.json
which re-uses the name of the root, but drops the dependencies since there are none.
Now the package
folder has all the requirements of being published as an NPM package. You can manually run npm publish
from that folder, or better yet setup a CD pipeline to publish and automate the versioning but that’s a topic for another post as there are many out there.
For this article we’ll just test it locally by running npm pack
to create a local package file.
Next lets create a simple server to test our package functionality.
Create the express server to serve the React app
git init express-server
cd express-server
npm init -y
npm i typescript express
npm i -D @types/express
npx tsc --init
2. Modify the tsconfig.json
to compile to the build folder.
"outDir": "./build"
3. Install the local package file you created above. Such as: (File path may be different)
npm i E://Repos/cra-package/package/cra-package-0.1.0.tgz
Ensure this adds and entry in the package.json
such as:
"cra-package": "file:../cra-package/package/cra-package-0.1.0.tgz",
Add src/index.ts
to represent our server using slightly modified version of the example given on the React docs but using our package paths.
We use the directoryPath
and defaultFilePath
from the package.
Now let’s add the npm scripts to run the server!
"start": "node build/index.js"
Run the build
(tsc) then start the server. You should see the default React app showing on your browser. You can go edit the React app and republish to see changes if you like.
You now have a process to create versioned react applications which leverage distributed by NPM.
Hope you learned something from this article.
If you’re interest in the code or I made a mistake in the article, here are the two repos to test final setup yourself.
Video
Some of the scripts for file may not have been clear so here is a video of it in action.