Home > Back-end >  How to create an npm package that supports both local and global (-g) install with webpack and types
How to create an npm package that supports both local and global (-g) install with webpack and types

Time:01-23

I am trying to create an npm package that will have a local and a global (-g) installation option, but I am confused at the difference between the four directories (src vs lib and the purpose of bin).

Before, I would have used the src directory, and have webpack transpile and bundle (ts-loader babel-loader) to the dist directory (with dist being hidden in gitignore). However, from what I looked at online, I should have instead bundle to lib directory, and manually create a bin directory that points to lib for executable cli (or global) packages?

Can someone tell me if my thought is correct? Should lib be added to gitignore, or should it be committed? What should I do about the bin directory? Is there a resource that you can point to for learning more about this?

I tried searching online for help with creating npm packages and searched through npmjs.com too, but I could not figure out what to do.

I also tried taking a quick look on github at what other projects did, but all I am able to derive so far is that the bin directory should include 1 index.js file that imports the main js file from the lib directory.

Thanks!

CodePudding user response:

It doesn't really matter much when it comes to directory structure. Further, I would highly recommend that you use just the compiler Babel or TypeScript and generate required bundle (CJS or ESM). This way, you easily gets to preserve your source code directory structure.

If you cannot just use the compiler, then prefer Rollup over Webpack as it is excellent when it comes to bundling for libraries. The ESM output is still experiment with Webpack (as of version 5.x.x).

If you still decide to go ahead with Webpack, then this is what I would do. For a given directory structure:

<ROOT>
  |-- src
  |    |-- app (actual library)
  |    |    |-- index.js
  |    |-- cli (cli related code)
  |    |    |-- index.js

And my content is:

// Library barrel file: src/app/index.js
export function main() {
  console.log('Hello from library');
}


// CLI barrel file: src/cli/index.js
import { main } from '../app/index';

function cli() {
  main();
}

cli();
// webpack.config.js
module.exports = {
  mode: 'production',
  entry: {
    'cli/index': './src/bin/cli.js',
    'library/index': './src/app/index.js'
  },

  output: {
    filename: '[name].js',
    library: {
      type: 'commonjs'
    }
  },

  externals: {
    '../app/index': {
      commonjs: '../app/index',
    },
  },

  // USE ONLY IF LIBRARY TARGET IS MODULE
  // experiments: {
  //   outputModule: true,
  // }

  // Rest of the configuration...
};

By default, it would create a dist folder with directory structure like this:

<ROOT>
  |-- dist
  |    |-- cli
  |    |    |-- index.js
  |    |-- library
  |    |    |-- index.js

The, I can make use of package.json files bin and main field to respective compiles files:

{
  "name": "my-lib",
  "version": "1.0.0",
  "main": "dist/library/index.js",
  "types": "dist/library/index.d.js",
  "bin": "dist/cli/index.js"
}

In above webpack configuration, note the use of externals which matches all the imports I have used. The default behavior of webpack is to always bundle and thus without this externals configuration, you would end up with library getting bundled twice for two entry points - one for cli and another as a library. (This is where Rollup helps greatly.) Further, you will have to do this for literally every third-party module that you imports or use webpack-node-externals

Few things to note:

  1. Generally avoid having both (CLI & library) in the same package. Use something like my-library and my-libary-cli. The my-libary-cli package can specify my-library as a peerDependency.
  2. As said earlier, what matters is the bin and main field. The folder structure doesn't matter.
  3. Also, prefer just compiler over bundler. If you need to import custom assets like PNG, CSS, etc. then only bundler would be required. In that case, prefer rollup over webpack due to the clean output that Rollup generates. Use webpack for application level bundling.
  4. To generate TS definition files, use tsc. Webpack won't do that for you.
  5. The setup can get complicated easily if you intend to use new exports field instead of main field. So, I would prefer having a barrel pattern instead of allowing multiple sub imports. May things get wrong. TypeScript, Jest setup, etc. on consumer side.
  • Related