powerbuoy
powerbuoy

Reputation: 12838

How to export multiple ES6 modules from one NPM package

I've built a relatively small NPM package consisting of roughly 5 different ES6 classes contained in one file each, they all look pretty much like this:

export default class MyClass {
    // ...
}

I've then setup an entry point for my package that looks like this:

export { default as MyClass } from './my-class.js';
export { default as MyOtherClass } from './my-other-class.js';

I've then run the entry point through webpack and babel ending up with a transpiled and minified index.js

Installing and importing the package works fine, but when I do the following from my client code:

import { MyClass } from 'my-package';

It doesn't just import "MyClass" it imports the entire file including all dependencies of every class (some of my classes have huge dependencies).

I figured this is how webpack works when you try to import parts of an already bundled package? So I set up my local webpack config to run node_modules/my-package through babel too and then tried:

import { MyClass } from 'my-package/src/index.js';

But even this imports every single class exported by index.js. The only thing that seems to work the way I want is if I do:

import MyClass from 'my-package/src/my-class.js';

But I'd much rather:

  1. Be able to import the transpiled and minified file so that I don't have to tell webpack to run babel inside node_modules and
  2. Be able to import each individual class directly from my entry point instead of having to enter the path to each file

What's the best practice here? How do others achieve similar setups? I've noticed GlideJS has an ESM version of its package which allows you to import only the things you need without having to run babel through it for example.

The package in question: https://github.com/powerbuoy/sleek-ui

webpack.config.js

const path = require('path');

module.exports = {
    entry: {
        'sleek-ui': './src/js/sleek-ui.js'
    },
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist'),
        library: 'sleek-ui', // NOTE: Before adding this and libraryTarget I got errors saying "MyClass() is not a constructor" for some reason...
        libraryTarget: 'umd'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            presets: ['@babel/preset-env']
                        }
                    }
                ]
            }
        ]
    }
};

package.json

  "name": "sleek-ui",
  "version": "1.0.0",
  "description": "Lightweight SASS and JS library for common UI elements",
  "main": "dist/sleek-ui.js",
  "sideEffects": false, // NOTE: Added this from Abhishek's article but it changed nothing for me :/
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode production"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/powerbuoy/sleek-ui.git"
  },
  "author": "Andreas Lagerkvist",
  "license": "GPL-2.0-or-later",
  "bugs": {
    "url": "https://github.com/powerbuoy/sleek-ui/issues"
  },
  "homepage": "https://github.com/powerbuoy/sleek-ui#readme",
  "devDependencies": {
    "@babel/core": "^7.8.6",
    "@babel/preset-env": "^7.8.6",
    "babel-loader": "^8.0.6",
    "webpack": "^4.42.0",
    "webpack-cli": "^3.3.11"
  },
  "dependencies": {
    "@glidejs/glide": "^3.4.1",
    "normalize.css": "^8.0.1"
  }
}

Upvotes: 27

Views: 12622

Answers (2)

CaitlinWeb
CaitlinWeb

Reputation: 76

I figured this is how webpack works when you try to import parts of an already bundled package?

Yes, the way you've got it set up is importing every class in index.js, which is then transpiled into one file (if it's targeting ES5, which is most common*). This means that when that file is imported in another file it comes in its entirety, with all those classes.

If you want proper tree-shaking you should avoid transpiling it into a CommonJS (ES5) bundle. My suggestion is to keep the ES6 modules, either by itself or in a separate location from the ES5 bundle. This article should help you fully understand this and has recommended instructions. Essentially it boils down to setting the Babel environment using preset-env (highly recommended if you're not already using it!) to preserve ES6 syntax. Here's the relevant Babel configuration, if you don't want to transpile to ES5:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "esmodules": true
        }
      }
    ]
  ]
}

The article details how to set up 2 bundles, each using a different module syntax.

Also worth noting, and is kind of mentioned in the article as well, you can set the ES module entry point in package.json. That tells Webpack/Babel where the ES6 modules can be found, which may be all you need for your use case. It seems conventional wisdom says to do:

{
  "main": "dist/sleek-ui.js",
  "module": "src/main.js"
}

But Node documentation has it as:

{
  "type": "module",
  "main": "dist/sleek-ui.js",
  "exports": {
    ".": "dist/sleek-ui.js",
    "./module": "src/main.js"
  }
}

If I had time I would play around with this and see which works correctly, but this should be enough to set you on the right path.


*ES5-targeted bundles are in CommonJS format, which has to include all the associated files, because ES5 doesn't have native module support. That came in ES2015/ES6.

Upvotes: 4

idancali
idancali

Reputation: 867

This is a valid use case. The ultimate goal being to do this import { MyClass } from 'my-package' but there's a cleaner way of doing this.

Create an aggregator index file in your my-package. Basically my-package/index.js and it should look like this:

import MyClass from './my-class.js'
import MyOtherClass from './my-other-class.js'

export { MyClass, MyOtherClass }

Then you can do import { MyClass } from 'my-package'. Easy peasy.

Have fun!

Upvotes: -3

Related Questions