Per-component NPM Packaging for Vue.js

August 28th 2020 Vue.js NuxtJS TypeScript

If you want to share your Vue.js components across multiple projects, you would typically package them into a component library. However, the people behind the Bit platform propose that distributing each component as a separate package is a better approach.

Leaving the advantages and disadvantages of this approach aside, there's still the issue of additional overhead with it unless you have good tooling to take care of that. That's what the Bit platform was designed for. Although it features support for multiple JavaScript frameworks, it seems to be primarily focused on React. For example, I couldn't get the generated packages for Vue.js working with server-side rendering.

Keeping that in mind in addition to the fact that their tooling makes you fully dependent on their paid service, I decided to try implementing a basic solution for per-component packaging myself to see what it would take to get it working. I started with my sample component library from a previous blog post.

I wanted to keep all the components in a single project like Bit manages to do it. Having a separate one for each component would introduce too much complexity to be worth it.

I'm no NPM expert, but I couldn't find a way to create multiple different NPM packages from the same root folder. A package is always built based on the package.json file in that folder. Even Lerna (which was designed for managing multi-package repositories) doesn't seem to overcome this limitation. It only makes it easier to handle multiple packages that reside in separate subfolders of the repository.

If I wanted to keep everything in the same project, my only remaining option was to modify the package.json file as needed before creating each package. Fortunately, that's exactly what npe does.

With some planning and assumptions, it would be enough to only change the name. All the other relevant properties could remain the same for every component (I explained them in more detail in my previous blog post):

{
  "files": ["dist/*"],
  "main": "./dist/vue-component.umd.js",
  "types": "./dist/index.d.ts"
}

For each component, I also had to create two files in the same folder:

  • index.ts exports the component to be packaged (along with all its dependencies):

    export { default as Button } from "./Button.vue";
    
  • index.d.ts provides a type description for that component:

    import { VueConstructor } from "vue";
    
    export const Button: VueConstructor;
    

The key part is then the build script that takes care of the whole process:

{
  "scripts": {
    "build:button": "npe name vue-component-button && vue-cli-service build --target lib --name vue-component src/components/button/index.ts && shx cp src/components/button/index.d.ts dist/index.d.ts && npm pack && shx mv ./vue-component-button-*.tgz .. && npe name vue-component"
  }
}

This is what each command does:

  • npe changes the name value in package.json.
  • vue-cli-service builds the component and drops the resulting files in the dist folder. The --name option makes sure that the files have the same name for each component so that the main value in package.json doesn't need to be changed.
  • shx cp copies the correct index.d.ts file to the dist folder.
  • npm pack creates a local package file (npm package would be used to publish it instead).
  • shx mv moves the generated package file to the parent folder where it's referenced from by the client project.
  • npe finally reverts the name value in package.json.

Each component would have a separate build script. With a consistent naming policy (e.g. build:<component-name>), another script for building all the components can be easily created with npm-run-all:

{
  "scripts": {
    "build-libs": "run-s build:*"
  }
}

A full working example is available in my GitHub repository.

As a prototype, this worked. The client project(s) could install the individual packages and use the components in them. However, this experiment is far from production-ready:

  • If a component in the library depends on another component, the dependency will be directly included in its package instead of being referenced from the dependency package. This shouldn't cause any conflicts in the client but could result in a lot of duplicated code.
  • All generated packages have the same version which makes it impossible to release new versions of only those components that changed.
  • Modifying the package.json file at build-time categorizes as a nasty heck, at least in my opinion.

It would take a significant amount of effort to fix all of these. Doing that would effectively result in creating a Bit alternative. It should be easier to fix the issues in Bit's Vue.js compiler instead. Or just stick with having all the components in a single library.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License