Output an ES module using webpack

59.2k views Asked by At

With Rollup I can output an ES module by simply setting the format option to 'es'. How can I do the same with webpack? If it's not possible now, does webpack have any plans to add it?

The only thing I've found in the documentation for output.libraryTarget that mentions ES modules is this:

libraryTarget: "commonjs-module" - Expose it using the module.exports object (output.library is ignored), __esModule is defined (it's threaded as ES2015 Module in interop mode)

However, it's rather unclear to me. Is it the same as libraryTarget: "commonjs2" with the only difference that __esModule is defined? What is "interop mode"?

5

There are 5 answers

3
Max Vorobjev On BEST ANSWER

Webpack2 does not have relevant libraryTarget yet, it does not output ES6 bundles. From the other side If you bundle your library in CommonJS bundlers wont be able to run Tree Shaking, not being able to eliminate unused modules. That's due to ES modules are still developing, so nobody ships ES bundles to browser, while webpack used primarily to create browser enabled bundles.

From the other side if you are publishing library you can provide both CommonJS (umd) and ES targets, thanks to "module" key in package. json. Actually you do not need webpack to publish ES target, all you need to do is to run babel on every file to get it to es2015 standart, for example if you are using react you can run babel with just "react" preset. If your source is already ES 2015 without extra features you can point module straight to your src/index.js:

//package.json
...
  "module": "src/index.js"
  "main": "dist/your/library/bundle.js
...

I found it convenient to use babel to handle export v from 'mod' instructions in my main index.js, so I have 1 module file exporting all my modules. That's achieved with babel-plugin-transform-export-extensions (also included in stage-1 preset).

I spot this approach from react-bootstrap library, you can see scripts in their github (they are webpack1). I have improved their scripts a little in my react-sigma repo, feel free to copy following files which will do what you need:

config/babel.config.js
scripts/buildBabel.js
scripts/es/build.js
scripts/build.js // this is command line controller, if you need just ES you don't need it

Also look at lib target (scripts/lib/build.js and .babelrc), I provide lib transpiled modules so library users can include only modules they need even without ES explicitly specifying require("react-sigma/lib/Sigma/"), especially useful if your lib is heavy and modular!

3
Pritish Vaidya On

Firstly I would like to state the difference between the commonJS and commonJS2

CommonJS doesn't support the use of module.exports = function() {} which is used by node.js and many other commonJS implementations.

Webpack2 employs the concept of the bundling the library code and for the widespread use of it and to make it compatible of working in different environments we use the --libraryTarget option

Now the part here will answer both your questions

The possible library options supported in webpack2 are

  • libraryTarget: "umd", // enum
  • libraryTarget: "umd-module", // ES2015 module wrapped in UMD
  • libraryTarget: "commonjs-module", // ES2015 module wrapped in CommonJS
  • libraryTarget: "commonjs2", // exported with module.exports
  • libraryTarget: "commonjs", // exported as properties to exports
  • libraryTarget: "amd", // defined with AMD defined method
  • libraryTarget: "this", // property set on this
  • libraryTarget: "var", // variable defined in root scope

Interlop has the following meaning

In order to encourage the use of CommonJS and ES6 modules, when exporting a default export with no other exports module.exports will be set in addition to exports["default"] as shown in the following example

export default test;
exports["default"] = test;
module.exports = exports["default"];

So basically it means that the commonJS-module can be used by exposing it as module.exports by using the interloping with ES2015 module wrapped in commonJS

More info about the interloping can be found in this blogpost and the stackoverflow link to it.

The basic idea is in ES6 runtime export and import properties cannot be changed but in commonJS this works fine as the require changes happen at runtime therefore it has ES2015 is interloped with the commonJS.

Update

Webpack 2 gives the option of creating the library which can be bundled and included.

If you want your module to be used in different environments then you can bundle it as a library by adding the library options and output it to your specific environment. Procedure mentioned in the docs.

Another simple example on how to use commonjs-module

Important point to note here is babel adds exports.__esModule = true to each es6 module and on importing it calls for the _interopRequire to check that property.

__esModule = true need to be set only on library export. It need to be set on the exports of the entry module. Inner modules don't need __esModule, it's just a babel hack.

As mentioned in the docs

__esModule is defined (it's threaded as ES2015 Module in interop mode)

Usage as mentioned in the test cases

export * from "./a";
export default "default-value";
export var b = "b";

import d from "library";
import { a, b } from "library";
2
Nate Levin On

What is the best non-hacky solution?

Apparently, this feature is coming in Webpack 5

Update: Webpack 5 was released, but still doesn't support this behavior.

Update 2: See this comment which makes it seem like it is coming soon.

Update 3: The newest version of Webpack should support this use case ! (Although it seems the implementation is still a bit buggy.

If you need to use older versions of Webpack you can use the steps described in this blog post to use a plugin which can allow for outputting an ES module.

When using the blog post above (the post's example code is a little off) we first run

npm i -D @purtuga/esm-webpack-plugin

We can then set webpack.config.js to the following:

const EsmWebpackPlugin = require("@purtuga/esm-webpack-plugin");
module.exports = {
    mode: "development",
    entry: "index.js",
    output: {
        filename: "consoleUtils.js",
        library: "LIB",
        libraryTarget: "var"
    },
    //...
    plugins: [
        new EsmWebpackPlugin()
    ]
}

Then we can just use the file like normal.

import func from "./bundle.js";

func();

See also this issue which is tracking this feature.

3
colxi On

If you don't mind adding an additional file to your package , you can use this workaround as a solution that allows you to distribute/import Webpack bundles as ES6 modules:

Configuration

webpack.config.js

output: {
    path: path.resolve('./dist'),
    filename: 'bundle.js',
    library: '__MODULE_DEFAULT_EXPORT__',
    libraryTarget: 'window',
    libraryExport: 'default'
  },

./dist/index.js (we need to create this file)

import './bundle.js'
const __MODULE_DEFAULT_EXPORT__ = window.__MODULE_DEFAULT_EXPORT__
delete window.__MODULE_DEFAULT_EXPORT__
export default __MODULE_DEFAULT_EXPORT__

package.json (important if you are going to distribute your module)

  "main": "dist/index.js",

How it works :

  • Webpack outputs the bundle to /dist/bundle.js, using the window as libraryTarget method. According to my configuration, this makes the package default export available as window.__MODULE_DEFAULT_EXPORT__, as soon as it's imported.
  • We create a custom "loader" : ./dist/index.js, which imports the /dist/bundle.js , picks the window.__MODULE_DEFAULT_EXPORT__ , deletes it from the window object (cleanup), assigns it to a local variable, and exports it as a ES6 export.
  • We configure our package.json to point to our "loader" : ./dist/index.js
  • We can now perform a regular import MyDefaultExportName from './dist/index.js'

Note : This solution -as it is exposed here- is far from being perfect, and has some downsides and limitations. Although there is space for improvements :)

0
Bill Keese On

With the latest Webpack V5 (as of September 2022), you can do this:

module.exports = {
  entry: ["./src/index.ts"],


  experiments: {
    outputModule: true,
  },

  output: {
    path: `${__dirname}/dist/myOutputFile.mjs`,
    library: {
      type: "module",
    },
  },
};

I also set devtool: false and used externals to keep things like React out of the bundle.

See https://github.com/webpack/webpack/issues/2933 and https://webpack.js.org/configuration/output/#outputlibrarytype for more details.