How to build multiple npm packages for sharing?

Viewed 645

I'm trying to create a mono repository to host multiple small packages that I plan to deploy on npm. I use Rollup for the bundling part.

After many hours watching what others do, searching on the internet and experimenting, I've reached a point where I'm stuck and little push in the right direction would be very much appreciated.


I've created a minimalist demo project that I've hosted on GitHub so it's easy to experiment with. You can find it here: https://github.com/Stnaire/my-lib


In the repository you'll find 3 packages in the packages directory:

  • config: contains a SharedConfiguration service
  • container: a wrapper around Inversify to have a statically accessible container with helper methods
  • storage: a package containing a StorageService (normally for writing in the local storage, cookies, etc) and a VarHolder helper which is just a memory storage.

In each package there is a package.json (defining the npm package parameters) and a tsconfig.json for the build.


What I'm trying to do is the have a npm package for each of the packages, each allowing for the following types of usages:

  1. With a bundler in a TypeScript environment
import { SharedConfiguration } from '@my-lib/config';

// or ideally:
import { SharedConfiguration } from '@my-lib/config/shared-configuration';
// this way only this dependency is included, not the whole `config` pacakge
  1. With a bundler in a JavaScript environment
var SharedConfiguration = require('@my-lib/config');

// Or like above, ideally:
var SharedConfiguration = require('@my-lib/config/shared-configuration'); 
  1. By including the output JS file in the browser
<script src="my-lib-config.umd.js"></script>
<script>
   MyLibConfig.SharedConfiguration.get(...);
</script>

What I tried

I've created two branches in the demo repository, corresponding to two strategies.

First strategy: create aliases (branch detailed-modules-resolution)

In the tsconfig.json, I do:

{
    "paths": {
        "@my-lib/config/*": ["packages/config/src/*"],
        "@my-lib/container/*": ["packages/container/src/*"],
        "@my-lib/storage/*": ["packages/storage/src/*"]
    }
}

This way I can import precisely what I need:

import { SharedConfiguration } from '@my-lib/config/shared-configuration';

And because the aliases also correspond to the folder structure in node_modules, it should work for the end user with a bundler as well.

But this way I get warnings when building as UMD:

No name was provided for external module '@my-lib/config/shared-configuration' in output.globals – guessing 'sharedConfiguration'
No name was provided for external module '@my-lib/container/container' in output.globals – guessing 'container'

Creating a global alias for each import is out of question, thus the second strategy.


Second strategy: centralize all public exports (branch centralized-imports)

So the idea is simply to export everthing the package wants to expose to other packages in the index.ts:

// packages/config/index.ts

export * from './shared-configuration';

Then in the build script, I do this:

// scripts/build/builds.js

/**
 * Define the mapping of externals.
 */
export const Globals = {
    'inversify': 'Inversify'
};
for (const build of Object.keys(Builds)) {
    Globals[`@my-lib/${Builds[build].package}`] = Builds[build].moduleName;
}

Wihch creates the following object:

{
    'inversify': 'Inversify',
    '@my-lib/config': 'MyLibConfig',
    '@my-lib/container': 'MyLibContainer',
    '@my-lib/storage': 'MyLibStorage',
}

This way umd builds are working.

But there is two main drawbacks:

  1. You have very little control on what you import. You import a whole package or nothing. And if the package depends on other packages you can quickly import thousands of lines for a little service.

  2. This creates circular dependencies, as in my example project. SharedConfiguration uses VarHolder which is in the @my-lib/storage. But this package also contain a StorageService which uses SharedConfiguration, creating a circular dependency because all the imports are based on the index.ts: @my-lib/config => @my-lib/storage => @my-lib/config.

I thought about using one strategy or the other depending if I build in umd or not, but it feels wrong. It must be simplier way to handle all of this.

Thank you very much for reading all this.

0 Answers
Related