DEV Community

Michael Bozhilov
Michael Bozhilov

Posted on

How to build and publish React TypeScript NPM packages with Vite

What is Vite? Most of you're probably familiar with what Vite is, but why not start with a quick introduction to get the flow going.

Vite (read as vit) is actually a combination of two great frontend tools - an immensely fast development server and a build command for shipping heavily optimized static assets using Rollup. Many developers have encountered the process of setting up a project using Create React App. While CRA can be useful for beginners due to its simplicity and abstraction of configuration, it has some drawbacks that outweigh its benefits, particularly its tendency to be bloated. Don't get me wrong, Vite is opinionated as well, but it's highly extensible through its Plugin API.

The main reason for writing this post is that although Vite is an excellent tool, it is still relatively new and sometimes lacks extensive documentation. When I initially had to set up a Vite npm package, it took me more time than I'd care to admit to get it working properly. Therefore, I've decided to create this post to help individuals who may be facing a similar situation and are looking for an alternative to Webpack.


Setting up the project:

yarn create vite

Image description

Since we want our project to be leveraging TypeScript's power, we will choose TypeScript + SWC as a variant. SWC is a new transpiler written in Rust which advertises itself to be even quicker than Esbuild (which as you know, it's quicker than the old kid on the block - Babel)

Since I've already set up a git repo for my package, I need to move all of the files from within the initialized project to the repo.

Image description


Directory structure

Let's clean up the project a little bit before creating a folder for our package.

Image description

In the picture above you can see I've removed the following files/directories from /src:

  • *.css and assets

If you find the predefined CSS styling to be beneficial, you can keep it as it is. However, in the majority of cases, I personally prefer to start with a clean slate, but this is purely a matter of personal preference.

Now, create a libs folder within the src directory.

Image description

Through this approach, we can encapsulate our package within the libs directory and test it locally within App.tsx (or any other file as a matter of fact). When I last worked on creating a component library, I utilized the src directory to establish an environment that closely resembled the one I intended to develop the library for.

IMPORTANT By utilizing yarn link (npm link), you can directly test the library in other projects and skip the above mentioned setup.

If you're getting the following exception, run yarn add --dev @types/react

Image description

Now, we should define an index.ts file which will be used to tell the package what to export (e.g. make available to the end user). In my case, I've created a React component defined in the file exampleComponent.tsx and I will be exporting it from index.ts as a default export. Alternatively, you have the option to export the component as a regular export. However, in this particular case, I am treating it as the primary export.

Image description

And index.ts looks like:

import ExampleComponent from "./exampleComponent";

export default ExampleComponent
Enter fullscreen mode Exit fullscreen mode

Now, we can locally test the component by importing it in App.tsx and starting the development server by running yarn dev:

Image description


Cool, cool, but now what?

We should play around a bit with Vite's configuration.

vite.config.ts:


import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { resolve } from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  build: {
    lib: {
      entry: resolve(__dirname, './src/libs/index.ts'),
      name: 'Example NPM Package',
      fileName: 'example-npm-package'
    },
    rollupOptions: {
      external: ['react'],
      output: {
        globals: {
          react: 'React'
        }
      }
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

As you can guess, in the above configuration we're defining the entry for our package (e.g. our index.ts file). The reason for specifying react as an external dependency is to exclude it from being bundled in the final distribution build. This decision is based on the assumption that since the package is intended for use in a React environment, 'react' is likely to be already installed and available in the consumer's project.


Add this to your package.json and rename all occurrences of example-npm-package to the value of fileName you have defined in the Vite config above:

  "name": "example-npm-package",
  "private": false,
  "version": "0.0.1",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "files": [
    "dist"
  ],
  "main": "./dist/example-npm-package.umd.cjs",
  "module": "./dist/example-npm-package.js",
  "exports": {
    ".": {
      "import": {
        "default": "./dist/example-npm-package.js",
        "types": "./dist/index.d.ts"
      },
      "require": {
        "default": "./dist/example-npm-package.umd.cjs",
        "types": "./dist/index.d.ts"
      }
    }
  },
  "types": "./dist/index.d.ts",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
Enter fullscreen mode Exit fullscreen mode

Now, due to us using "bundler" as a module resolution method (this has been default for Vite for some time now), we need to explicitly point to the types when generating the exports.
Otherwise, you will get the following exception when loading the package:

There are types at '.../node_modules/example-npm-package/dist/index.d.ts', but this result could not be resolved when respecting package.json "exports". The 'example-npm-package' library may need to update its package.json or typings.


Now, let's try building our package.

vite build && tsc src/libs/index.ts --declaration --emitDeclarationOnly --jsx react --esModuleInterop --outDir dist
Enter fullscreen mode Exit fullscreen mode

Here we use the typescript compiler to generate type definitions for all of our exports. You may add the command as a script in your package.json file to make your life easier.

What should happen now is that a new folder called dist should have been generated with your bundled javascript code and the generated type definitions.

If your build fails with some of the following exceptions:

node_modules/@types/react/index.d.ts:246:10 - error TS2456: Type alias 'ReactFragment' circularly references itself.

246     type ReactFragment = Iterable<ReactNode>;
             ~~~~~~~~~~~~~

node_modules/@types/react/index.d.ts:246:26 - error TS2304: Cannot find name 'Iterable'.

246     type ReactFragment = Iterable<ReactNode>;
                             ~~~~~~~~

node_modules/@types/react/index.d.ts:254:10 - error TS2456: Type alias 'ReactNode' circularly references itself.

254     type ReactNode =
             ~~~~~~~~~

node_modules/@types/react/index.d.ts:446:23 - error TS2583: Cannot find name 'Set'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later.

446         interactions: Set<SchedulerInteraction>,
                          ~~~
Enter fullscreen mode Exit fullscreen mode

You may need to do yarn add @types/node


Let's push the package to NPM

Run yarn publish and you will be asked for your npm credentials.

Image description

You're all set!
You can now fetch your React npm package with type definitions!

Here's a link to the template repo. Feel free to ask me any questions or open Pull Requests if there is anything you would like to improve to the template!

Top comments (1)

Collapse
 
psychosynthesis profile image
Nick

In fact, this is an extremely lousy guide.

Firstly, it’s not clear why there is yarn in this chain at all? Everything described can be done by the vite + tsc combination. Secondly, how do I get vite to export CSS?