How to support subpath imports using React+Rollup+Typescript

Chong Lu Khei
Government Digital Services, Singapore
5 min readAug 2, 2022
Photo by Viktor Talashuk from Unsplash

Singapore Design System (SGDS) recently launched v2.0.0 and its corresponding React component library.

In the early stages, packaging our React+Typescript library with Rollup was straight forward — feed Rollup with a single entry point as input and generate a single output. Done!

Oh how nice would that be! We could even use TSDX that uses rollup under the hood. Then, came a slew of problems : high import cost, node_modules at war, SSR apps throwing “import” errors, lack of types definition support for conditional exports.

Several failed attempts and many researches later, we finally found the solution. We looked into how the popular React component libraries did it and the crux of the solution lies in package.json. We wonder why there aren’t any articles / tutorials mentioning it other than this one post on StackOverflow.

This post documents the steps taken to build a hybrid NPM package (supports ESM + CJS) from React component library that supports subpath imports. The library is built with React + Typescript and Rollup as module bundler.

The emphasis on this post is NOT:

  • How to use React +Typescript + Rollup(there are plenty of good resources and tutorial online)

Pre-requisites

  • You have some knowledge on how Rollup works
  • You have some knowledge on publishing libraries via npm
  • You are here because you want to know how to configure subpath imports for a hybrid NPM package

What are subpath imports?

We refer to “subpath imports” as importing individual components like @govtechsg/sgds-react/Button rather than the entire library @govtechsg/sgds-react

Why subpath imports?

We started with the most basic configuration of rollup that bundles and consolidates all our components from a single input to generate a single output.

Result? The entire library was loaded. This means high import cost which could impact performance. There is a need to reduce code sent up to the client!

Solution? Like many other libraries (lodash, react-bootstrap, materialUI), our library should support subfolder imports like below.

Early Challenges

There are plenty of online resources on how to package hybrid NPM packages but none talked about how to also support subpath imports

This tutorial will present how we get subfolder import working with React+Typescript+Rollup while highlighting the challenges faced in each of the option that we considered during bundling.

  1. Project Directory Structure

Compartmentalise each main component into a folder with an entry point index.ts. In each index.ts, export all the necessary components of the main component. Name each folder the appropriate name for the component as this will be crucial when configuring the subpath imports later.

Add an entry point for src folder which exports all the components from the individual entry points

src├── Accordion│   ├── Accordion.tsx│   ├── AccordionBody.tsx│   ├── AccordionButton.tsx│   ├── AccordionCollapse.tsx│   ├── AccordionContext.ts│   ├── AccordionHeader.tsx│   ├── AccordionItem.tsx│   ├── AccordionItemContext.ts│   └── index.ts├── Alert│   ├── Alert.tsx│   └── index.ts├── Badge│   ├── Badge.tsx│   └── index.ts├── index.ts

example code in src/Accordion/index.ts

example code in src/index.ts

The project directory structure is crucial in the rollup configuration later on

2. tsconfig.json

Specify the output directory and declaration: true to output .d.ts declaration files for consumers

3. package.json (root)

Specify the file paths for main, module and typings.

main: points to the entry point for cjs format of the library ,

module: points to the entry point for esm format of the library

typings: points to the entry point for type definitions of the above two files

Under scripts specify the build and post:build commands. Here we are using rollup commands. More on this later.

"scripts": {"build": "npm run rollup && npm run post:build","post:build": "node ./scripts/frankBuild.js","rollup": "rm -rf dist && rollup -c",...},"main": "dist/cjs/index.js","module": "dist/index.js","typings": "dist/index.d.ts",

At this point, subpath patterns using conditional exports was also explored but it was not ideal as types definition file could not be specified for individual subpaths at the moment.

You read about the package.json type = "module" and exports keywords which will magically make everything work, but they don't work as advertised

I’ve tried the .mjs and .cjs extensions which fail with more than a few essential build tools.

Like the author in above article, we tried the above but it doesn’t work for us too. It threw an error in SSR apps.

Server Error on NextJS in one of our attempts to package the library

This article explains well why it the above quoted methods didn’t work.

4. rollup.config.js

The library was to provide esm and cjs formats. Set the library’s entry point as the input and set the respective formats to output. Generate outputs for individual components by using the entry point of each component, see folderBuilds on line 25 below.

getFolders() returns an array of folder names from ./src

Plugins

For subfolder, an additional plugin was used

This is the crucial step to support subdirectory imports. This plugin helps to generate a package.json per component. Specify the correct paths for main , module , types

5. Build package

Run npm run build to generate the dist/ folder for publishing. Remember to copy relevant contents (main, module, typings, version , name, dependencies, etc.) from root folder’s package.json to dist folder’s package.json.

In our case, we created a post:build script to handle this, see here

Final output:

We are providing subpath imports for esm formats only, but could improve it further to include for cjs with the same method.

6. Publish

Publish the dist folder. From root folder, run

npm publish ./dist

See the full source code here.

--

--