How to publish a module written in ES6 to NPM?

43,542

Solution 1

The pattern I have seen so far is to keep the es6 files in a src directory and build your stuff in npm's prepublish to the lib directory.

You will need an .npmignore file, similar to .gitignore but ignoring src instead of lib.

Solution 2

I like José's answer. I've noticed several modules follow that pattern already. Here's how you can easily implement it with Babel6. I install babel-cli locally so the build doesn't break if I ever change my global babel version.

.npmignore

/src/

.gitignore

/lib/
/node_modules/

Install Babel

npm install --save-dev babel-core babel-cli babel-preset-es2015

package.json

{
  "main": "lib/index.js",
  "scripts": {
    "prepublish": "babel src --out-dir lib"
  },
  "babel": {
    "presets": ["es2015"]
  }
}

Solution 3

TL;DR - Don't, until ~October 2019. The Node.js Modules Team has asked:

Please do not publish any ES module packages intended for use by Node.js until [October 2019]

2019 May update

Since 2015 when this question was asked, JavaScript support for modules has matured significantly, and is hopefully going to be officially stable in October 2019. All other answers are now obsolete or overly complicated. Here is the current situation and best practice.

ES6 support

99% of ES6 (aka 2015) has been supported by Node since version 6. The current version of Node is 12. All evergreen browsers support the vast majority of ES6 features. ECMAScript is now at version 2019, and the versioning scheme now favors using years.

ES Modules (aka ECMAScript modules) in browsers

All evergreen browsers have been supporting import-ing ES6 modules since 2017. Dynamic imports are supported by Chrome (+ forks like Opera and Samsung Internet) and Safari. Firefox support is slated for the next version, 67.

You no longer need Webpack/rollup/Parcel etc. to load modules. They may be still useful for other purposes, but are not required to load your code. You can directly import URLs pointing to ES modules code.

ES modules in Node

ES modules (.mjs files with import/export) have been supported since Node v8.5.0 by calling node with the --experimental-modules flag. Node v12, released in April 2019, rewrote the experimental modules support. The most visible change is that the file extension needs to be specified by default when importing:

// lib.mjs 

export const hello = 'Hello world!';

// index.mjs:

import { hello } from './lib.mjs';
console.log(hello);

Note the mandatory .mjs extensions throughout. Run as:

node --experimental-modules index.mjs

The Node 12 release is also when the Modules Team asked developers to not publish ES module packages intended for use by Node.js until a solution is found for using packages via both require('pkg') and import 'pkg'. You can still publish native ES modules intended for browsers.

Ecosystem support of native ES modules

As of May 2019, ecosystem support for ES Modules is immature. For example, test frameworks like Jest and Ava don't support --experimental-modules. You need to use a transpiler, and must then decide between using the named import (import { symbol }) syntax (which won't work with most npm packages yet), and the default import syntax (import Package from 'package'), which does work, but not when Babel parses it for packages authored in TypeScript (graphql-tools, node-influx, faast etc.) There is however a workaround that works both with --experimental-modules and if Babel transpiles your code so you can test it with Jest/Ava/Mocha etc:

import * as ApolloServerM from 'apollo-server'; const ApolloServer = ApolloServerM.default || ApolloServerM;

Arguably ugly, but this way you can write your own ES modules code with import/export and run it with node --experimental-modules, without transpilers. If you have dependencies that aren't ESM-ready yet, import them as above, and you'll be able to use test frameworks and other tooling via Babel.


Previous answer to the question - remember, don't do this until Node solves the require/import issue, hopefully around October 2019.

Publishing ES6 modules to npm, with backwards compatibility

To publish an ES module to npmjs.org so that it can be imported directly, without Babel or other transpilers, simply point the main field in your package.json to the .mjs file, but omit the extension:

{
  "name": "mjs-example",
  "main": "index"
}

That's the only change. By omitting the extension, Node will look first for an mjs file if run with --experimental-modules. Otherwise it will fall back to the .js file, so your existing transpilation process to support older Node versions will work as before — just make sure to point Babel to the .mjs file(s).

Here's the source for a native ES module with backwards compatibility for Node < 8.5.0 that I published to NPM. You can use it right now, without Babel or anything else.

Install the module:

npm install local-iso-dt
# or, yarn add local-iso-dt

Create a test file test.mjs:

import { localISOdt } from 'local-iso-dt/index.mjs';
console.log(localISOdt(), 'Starting job...');

Run node (v8.5.0+) with the --experimental-modules flag:

node --experimental-modules test.mjs

TypeScript

If you develop in TypeScript, you can generate ES6 code and use ES6 modules:

tsc index.js --target es6 --modules es2015

Then, you need to rename *.js output to .mjs, a known issue that will hopefully get fixed soon so tsc can output .mjs files directly.

Solution 4

@Jose is right. There's nothing wrong with publishing ES6/ES2015 to NPM but that may cause trouble, specially if the person using your package is using Webpack, for instance, because normally people ignore the node_modules folder while preprocessing with babel for performance reasons.

So, just use gulp, grunt or simply Node.js to build a lib folder that is ES5.

Here's my build-lib.js script, which I keep in ./tools/ (no gulpor grunt here):

var rimraf = require('rimraf-promise');
var colors = require('colors');
var exec = require('child-process-promise').exec;

console.log('building lib'.green);

rimraf('./lib')
    .then(function (error) {
        let babelCli = 'babel --optional es7.objectRestSpread ./src --out-dir ./lib';
        return exec(babelCli).fail(function (error) {
            console.log(colors.red(error))
        });
    }).then(() => console.log('lib built'.green));

Here's a last advice: You need to add a .npmignore to your project. If npm publish doesn't find this file, it will use .gitignore instead, which will cause you trouble because normally your .gitignore file will exclude ./lib and include ./src, which is exactly the opposite of what you want when you are publishing to NPM. The .npmignore file has basically the same syntax of .gitignore (AFAIK).

Solution 5

Following José and Marius's approach, (with update of Babel's latest version in 2019): Keep the latest JavaScript files in a src directory, and build with npm's prepublish script and output to the lib directory.

.npmignore

/src

.gitignore

/lib
/node_modules

Install Babel (version 7.5.5 in my case)

$ npm install @babel/core @babel/cli @babel/preset-env --save-dev

package.json

{
  "name": "latest-js-to-npm",
  "version": "1.0.0",
  "description": "Keep the latest JavaScript files in a src directory and build with npm's prepublish script and output to the lib directory.",
  "main": "lib/index.js",
  "scripts": {
    "prepublish": "babel src -d lib"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.5.5",
    "@babel/core": "^7.5.5",
    "@babel/preset-env": "^7.5.5"
  },
  "babel": {
    "presets": [
      "@babel/preset-env"
    ]
  }
}

And I have src/index.js which uses the arrow function:

"use strict";

let NewOneWithParameters = (a, b) => {
  console.log(a + b); // 30
};
NewOneWithParameters(10, 20);

Here is the repo on GitHub.

Now you can publish the package:

$ npm publish
...
> [email protected] prepublish .
> babel src -d lib

Successfully compiled 1 file with Babel.
...

Before the package is published to npm, you will see that lib/index.js has been generated, which is transpiled to es5:

"use strict";

var NewOneWithParameters = function NewOneWithParameters(a, b) {
  console.log(a + b); // 30
};

NewOneWithParameters(10, 20);

[Update for Rollup bundler]

As asked by @kyw, how would you integrate Rollup bundler?

First, install rollup and rollup-plugin-babel

npm install -D rollup rollup-plugin-babel

Second, create rollup.config.js in the project root directory

import babel from "rollup-plugin-babel";

export default {
  input: "./src/index.js",
  output: {
    file: "./lib/index.js",
    format: "cjs",
    name: "bundle"
  },
  plugins: [
    babel({
      exclude: "node_modules/**"
    })
  ]
};

Lastly, update prepublish in package.json

{
  ...
  "scripts": {
    "prepublish": "rollup -c"
  },
  ...
}

Now you can run npm publish, and before the package is published to npm, you will see that lib/index.js has been generated, which is transpiled to es5:

'use strict';

var NewOneWithParameters = function NewOneWithParameters(a, b) {
  console.log(a + b); // 30
};

NewOneWithParameters(10, 20);

Note: by the way, you no longer need @babel/cli if you are using the Rollup bundler. You can safely uninstall it:

npm uninstall @babel/cli
Share:
43,542

Related videos on Youtube

Traveling Tech Guy
Author by

Traveling Tech Guy

I'm a technologist, entrepreneur and developer. I started 3 startups, and currently assist other companies through Traveling Tech Guy - my software consulting company. I specialize in web and mobile development, integration projects, and managing software development projects of various sizes and complexities. I'm always looking for the next challenge, and always eager to learn a new technology/stack/paradigm. Most recently I've been using NodeJS, Express, React and various JavaScript frameworks. Prior to that, I developed in Scala, PHP, and several years of .Net in the enterprise. My mobile experience includes iOS, Android and WP. I've been an avid Stack Overflow user almost from day 1. I'm thankful to the community for helping me out of sticky code situations, and hope that I can contribute back by answering as many questions as I can - on Stack Overflow, Web Applications, Super User, and Android Enthusiasts.

Updated on January 14, 2020

Comments

  • Traveling Tech Guy
    Traveling Tech Guy over 4 years

    I was about to publish a module to NPM, when I thought about rewriting it in ES6, to both future-proof it, and learn ES6. I've used Babel to transpile to ES5, and run tests. But I'm not sure how to proceed:

    1. Do I transpile, and publish the resulting /out folder to NPM?
    2. Do I include the result folder in my Github repo?
    3. Or do I maintain 2 repos, one with the ES6 code + gulp script for Github, and one with the transpiled results + tests for NPM?

    In short: what steps do I need to take to publish a module written in ES6 to NPM, while still allowing people to browse/fork the original code?

    • talves
      talves almost 9 years
      I have been struggling with this decision lately. I am seeing the answer you marked as correct by José being the consensus also.
    • Dan Dascalescu
      Dan Dascalescu over 5 years
      Here's my 2018 answer, taking into account the progress with module support since 2015.
    • SeanMC
      SeanMC about 5 years
      I'd love if I could do the opposite. Use an ES Module to import an NPM module, but these are the only results I get.
  • jfriend00
    jfriend00 about 9 years
    This doesn't seem to answer the question. I think the OP is trying to figure out how to structure their Github repo and what to publish to NPM and all you've kind of said is that they can do whatever they want. The OP wants specific recommendations on a good practice for this situation.
  • JoshWillik
    JoshWillik about 9 years
    @jfriend00 I disagree. I've recommended that he transpile, and only publish the files that are required for require( 'package' ) to work. I'll edit my answer to make this more clear. That said, Jose's answer is much better than my own.
  • Sukima
    Sukima over 8 years
    Any commands in the scripts will have node_modules/.bin added to their $PATH and since babel-cli installs a binary to node_modules/.bin/babel there is no need to reference the command by path.
  • seldon
    seldon about 8 years
    Please notice that prepublish is problematic because it could run at install time (github.com/npm/npm/issues/3059), prefer the more idiomatic version script hook (docs.npmjs.com/cli/version)
  • Ahmed Abbas
    Ahmed Abbas almost 8 years
    Do you have example repository?
  • Jordan Gray
    Jordan Gray almost 8 years
    José's answer is very good, but I appreciate this one for explicitly outlining good rules-of-thumb for when/why to use one vs. two packages.
  • sonlexqt
    sonlexqt about 7 years
    @mattecapu it seems like the problem with prepublish is still there. At the moment I think manually compile the src directory and npm publish is the way to go.
  • phazonNinja
    phazonNinja about 7 years
    @JamesAkwuh Note that you will likely want to change the "start" and "build" commands in the package.json to use the relative path of the babel-cli: ./node_modules/babel-cli/bin/babel.js -s inline -d lib -w src. This should ensure that installs don't fail when deploying to new environments.
  • James Akwuh
    James Akwuh about 7 years
    @phazonNinja npm handles it
  • Alex Mann
    Alex Mann about 7 years
    You can use prepublishOnly script hook (see docs.npmjs.com/misc/scripts#prepublish-and-prepare). Note that in version 5 of npm this should function as expected, but for now (assuming you're using npm v4+) this should work.
  • Frank Nocke
    Frank Nocke almost 7 years
    “If there's no .npmignore file, but there is a .gitignore file, then npm will ignore the stuff matched by the .gitignore file.” official npm docs
  • Frank Nocke
    Frank Nocke almost 7 years
    @marius-craciunoiu, I still struggle, on which end the es5 transpilation is meant to happen? – I can see loads of projects, building (directly or indirectly through build target) in the prepublish script. But nobody (except for <1% I deem in error) commits the lib/ folder, as I can see from each respective repository... — thus prepublish is more of a syntax check, not a pre-rendering (or: „pre-built binaries“) ?
  • Frank Nocke
    Frank Nocke almost 7 years
    How does package.json - "files" (docs) interfere with .npmignore?
  • AC88
    AC88 almost 7 years
    Compile on install is very rude. Please don't do this - npm install time is bad enough! For internal code where you want to avoid using an npm package repository, you can: 1) use a monorepo, 2) Upload and depend on npm pack output, 3) check in build output.
  • AC88
    AC88 almost 7 years
    @FrankNocke from the docs, "even if they would be picked up by the files array", so "if in files and not in ignore", so you could, eg add lib to files and *.doc.xml to .npmignore.
  • AC88
    AC88 almost 7 years
    @FrankNocke prepublish runs before publish (obv.), which pushes stuff to npm (or wherever you configure). So it's for building what goes in the NPM package, even if it's not checked in.
  • Dan Dascalescu
    Dan Dascalescu almost 7 years
    Instead of .npmignore you can use the files field in package.json. It lets you specify exactly the files you want to publish, instead of hunting for random files you don't want to publish.
  • Dan Dascalescu
    Dan Dascalescu almost 7 years
    Instead of .npmignore you can use the files field in package.json. It lets you specify exactly the files you want to publish, instead of hunting for random files you don't want to publish.
  • Vlad
    Vlad over 6 years
    A good article (github.com/bookercodes/articles/blob/master/…) suggests putting babel compilation into the "build" script, while executing it from "prepublish": "npm run build" or "yarn build" if your use yarn instead.
  • Dinesh Pandiyan
    Dinesh Pandiyan about 6 years
    I have a boilerplate repo that does exactly as what's mentioned in this answer and it's the recommended approach as well - npm-module-boilerplate
  • Dan Dascalescu
    Dan Dascalescu over 5 years
    That post omits the fact that you can (and should) publish to NPM modules authored in ES6 and importable directly without requiring Babel or any other transpilers.
  • Pedro Pedrosa
    Pedro Pedrosa over 5 years
    Saying "All evergreen browsers support the vast majority of ES6 features." doesn't mean much when you look at the data and realise that es6 support in browsers only reaches about 80% of all users.
  • Joe
    Joe over 5 years
    Wont this break tree-shaking?
  • thisismydesign
    thisismydesign almost 5 years
    I don't recommend using .npmignore, try package.json's files instead, see: github.com/c-hive/guides/blob/master/js/…
  • thisismydesign
    thisismydesign almost 5 years
    Currently, the ecosystem is definitely not mature enough for this. The Node.js team with the release of v12 specifically asked: "Please do not publish any ES module packages intended for use by Node.js until this is resolved." 2ality.com/2019/04/nodejs-esm-impl.html#es-modules-on-npm Mocha doesn't natively support .mjs files. Many-many libraries (e.g. create-react-app, react-apollo, graphql-js) had issues with dependencies containing mjs files. Node.js plans to roll out official support in October 2019 which is the earliest I would seriously revisit this.
  • Dan Dascalescu
    Dan Dascalescu almost 5 years
    @thisismydesign: that's exactly what I had recommended in my comment above..?
  • thisismydesign
    thisismydesign almost 5 years
    My bad, didn't notice :)
  • kyw
    kyw over 4 years
    How would you integrate Rollup bundler?
  • Yuci
    Yuci over 4 years
    @kyw, regarding how to integrate Rollup bundler, see my updated answer.
  • a.barbieri
    a.barbieri about 4 years
    December 2019 update --> github.com/rollup/rollup/blob/…
  • Joakim L. Christiansen
    Joakim L. Christiansen almost 4 years
    I can't seem to import globally installed ES6 modules in the node_modules folder (provided by npm root -g). Are we really not supposed to be able to do that? I am really confused. I know npm link can solve the problem by linking the module to my local node_modules folder, but I want to know why importing global node modules is not supported.
  • Joakim L. Christiansen
    Joakim L. Christiansen almost 4 years
    Answering myself, I guess it will never be supported: github.com/nodejs/node-eps/blob/master/… It's a really stupid decision though, would be easy to support...
  • Emmington
    Emmington over 3 years
    @JoakimL.Christiansen Namaste. Having similar hardship due to the completely misarticulated official stance with regard to modules by the node community, I followed that link, and yes, I was disappointed, but not being satisfied, I checked the publish date of that linked article... 9 Aug 2017. Just pointing that out as the linked article could be a red herring so to speak...
  • crs1138
    crs1138 about 3 years
    Thanks for this! Just to clarify, to integrate your function in another project you can just use import NewOneWithParameters from 'latest-js-to-npm'; ? So in your code you don't actually need export?
  • zr0gravity7
    zr0gravity7 over 2 years
    "All other answers are now obsolete or overly complicated. Here is the current situation and best practice." This seems highly subjective and definitely does not reflect the state of npm packages being published in 2021.
  • zr0gravity7
    zr0gravity7 over 2 years
    Note that the built-in prepublish script has been deprecated in favour of prepare. See here for an overview of the change: iamakulov.com/notes/npm-4-prepublish
  • VimNing
    VimNing about 2 years
    For readers in 2022: This answer probably needs an update. In package.json one can set "type": "module" to read .js as .mjs by default. And maybe try to simplify it a bit, it's very verbose and misleading for me.