Publishing a React library

Harun Doğanay
6 min readFeb 23, 2023

--

Managing the tools that we use for running, building, bundling, and publishing libraries in the frontend is a different process than developing components. There is no single truth of bundling for libraries. The tools that you use may vary. A combination of different tools may conflict and require additional configurations. It is so crucial to follow up the path that you follow. It makes life easier if you are familiar with the inputs and outputs of each tool. I am going to explain each step deeply.

What is the difference between an application and a library?

If you ever created a React project, most probably it was a React application. It means you have a dev server for running the application locally and a static entry point for serving in the production environment.

You may need to make unique solutions for your use cases. Then in this scenario, you may not use the config examples you find online. It is so crucial to understand each step and be aware of what you are doing.

Monorepo or not?

Monorepo is a structure and tool combination that is how you manage your packages. From the bundling tool perspective, it doesn’t matter if it is monorepo or not. Because your monorepo packages are still independent packages and we still manage dependency management through package.json. You don’t have to build and publish individual packages if you don’t change any code inside a package.

Since we are focusing on library publication, our private packages also should be libraries. Because the application that is going to use our library is going to resolve dependency trees through our application. Therefore, our sub-packages are going to be fetched from the npm repository.

There is another way to handle internal dependencies. If we put our internal dependency to devDependencies, then the application doesn’t try to install sub-packages. But the inclusion of the sub-package should be handled manually with bundling tools. It’s up to you how you handle the whole process.

For our case, we are going to use monorepo for our library.

Let’s first start with the basic definition of the package to talk in the same language.

What is the package?

The package is a folder structure that is described with package.json. The package is a folder that contains a package.json and subfolders until having another package.json and node_modules.

It is crucial to understand how NodeJS treats files by looking at which package.json.

What do we have in the package.json?

We have root package.json which is actually a package definition for monorepo. Not for library packages. We can put some dev-dependencies which we are going to use in the development phase and scripts that we can run for executing the monorepo tool commands.

For the packages in monorepo, it is a good practice to scope packages under a namespace. Like ‘@my-company/common’. Package name config belongs to the ‘name’ property. It is not surprising, right?

The ‘files’ property takes a crucial role when we publish our package. It is an allowlist to included files and folders in an npm release. If you don’t specify it, All your source code and config files will be included in your release. It is not preferred.

I assume that you use typescript and want to publish your type definitions also. The consumer can now your types with looking at the ‘types’ property in your package.json. It should be the entry point for your type definitions.

At last but not least, actually, the most important point is the ‘main’ and ‘module’ properties. ‘main’ should address to the CommonJS bundle and the ‘module’ for ESModule.

But more importantly, the ‘exports’ property allows us to define conditional exports. It’s worth checking how nodejs node resolution algorithm works and how to configure conditional exports for the package. In the upcoming steps, we’ll understand why we might need this.

Define node engine version

If you want to be on the safe side and your code has node version-specific development, it’d be best to define node version on the ‘node’ property. If you define your node version as v18, then the application that uses node v16 cannot build the application. Because our minimum requirement is v16.

NodeJs Module Types

Node.js has two module systems: CommonJS modules and ECMAScript modules (ESModule).

You can tell NodeJs which module to use with the package.json type field. Nodejs determines the module type by looking at the extension of the js file and the nearest parent (or in the same folder) package.json type field. If the type is set to a module, then nodejs treats it as an es module. If the nearest package.json doesn’t have a type field or is specified as a CommonJS, nodejs treats it as a CommonJS module.

File extensions have always had precedence over package.json module type. It means .mjs files are treated as an es module even module type is set to commonjs or vice versa.

Dual CommonJS/ES module configuration

We can define our module entry point by the ‘main’ and ‘exports’ fields. Both can be used for CommonJS and ES modules. Exports take precedence over the main and can be used for defining multiple entry points. It is also possible to decide on module type whether the package is referenced via import or require. Of course, we have to create both CommonJS and ES javascript bundle sources. We are going to dig out how to create sources soon.

Bundling for a library?

If the topic is a library rather than an application, Webpack is not the most convenient option. We use ViteJS as a bundler. It is lightning-fast and easy to configure. It can be used for both application and library outputs. For vite configuration just follow the instruction on their documentation.

Here I’m going to explain how to fine-tune library build.

build: {
lib: {
entry: path.resolve(__dirname, 'src/index.ts'),
name: 'MyAwesomePackage',
formats: ['es', 'umd'],
fileName: (format) => `index.${format}.js`
}
}

build.lib.entry: main entry point to the builder. It can be any file that you expose your components. For our case, it is ‘src/index.ts’

build.lib.formats: Vite is going to create modules that we parameterized.

build.lib.filename: This is going to be our output bundle file. Attention! This should be the same as the entry point of the package. Do you remember the package.json exports property? Bingo! Vite is going to create esmodule and commonjs output. Then we name it with format extension. So nice!

build: {
rollupOptions: {
output: {
exports: 'named',
globals: {
react: 'React',
'react-dom': 'ReactDOM',
'styled-components': 'styled'
}
}
external: ['react', 'react-dom', 'styled-components']
}
}

Rollup? Yes, ViteJS is using RollupJS under the hood. Thereby we can config the build using all Rollup config options. Worths to check the Rollup config documentation.

If we make React external, Vite is not going to include React code in our bundle. We say, ‘Hey Vite! Calm down man, don’t complain if you cannot find React reference in your bundle, I’ll provide you some React from your consumer. Aaaand its name is React!’ (as I give you in global property).

Should I put my private packages in external?

If you put your dependency in the monorepo to the external field, you have to put that dependency in package.json dependencies. Otherwise, it should be the devDependency.

I agree with you it is a little complicated task to handle shared dependencies. That’s why it is so crucial to know the package dynamics, node dependency management, and module resolution. package.json and vite configuration should be synced with each other.

Obviously, our package is a React project. I guess our consumers are also React projects. They may use different React versions and the styled-components. To be more explicit and to get meaningful error messages in case, it is a good idea to put react, react-dom, and styled-components dependencies to the peerDependencies.

Bundling with dependencies or not?

There are 2 options to handle dependencies. Include them in your bundle, or delegate the dependency to the consumer. If you delegate it, you have to put your dependencies to the peerDependencies. If you bundle with your dependencies, the application (consumer) will end up with duplicated code. Maybe the application will solve deduplication or three-shaking. But you give responsibility to the application. It’s your decision…

ps: Every part of this article is open for discussion and constructive comments. Feel free to make comments if you don’t agree with me.

--

--