如何将用 ES6 编写的模块发布到 NPM?

How to publish a module written in ES6 to NPM?

我正要向 NPM 发布一个模块,当我想到用 ES6 重写它时,既要面向未来,又要学习 ES6。我使用 Babel 转译为 ES5,并进行了 运行 测试。但我不确定如何进行:

  1. 我是否转译并将生成的 /out 文件夹发布到 NPM?
  2. 我是否将结果文件夹包含在我的 Github 存储库中?
  3. 或者我是否维护 2 个存储库,一个包含 ES6 代码 + gulp 用于 Github 的脚本,另一个包含转译结果 + NPM 测试?

简而言之:我需要采取什么步骤才能将用 ES6 编写的模块发布到 NPM,同时仍然允许人们 browse/fork 原始代码?

到目前为止,我所看到的模式是将 es6 文件保存在 src 目录中,并在 npm 的预发布中构建你的东西​​到 lib 目录。

您将需要一个 .npmignore 文件,类似于 .gitignore 但忽略 src 而不是 lib

NPM 包的两个标准是它可以与 require( 'package' ) 一起使用,并且可以做一些类似软件的事情。

如果你满足这两个要求,你可以为所欲为。 即使模块是用 ES6 编写的,如果最终用户不需要知道这一点,我也会暂时转译它以获得最大的支持。

但是,如果像 koa 一样,您的模块需要与使用 ES6 功能的用户兼容,那么两个包的解决方案可能是更好的主意。

外卖

  1. 只发布使 require( 'your-package' ) 正常工作所需的代码。
  2. 除非 ES5 和 6 之间对用户很重要,否则只发布 1 个包。如果需要,请转译它。

@Jose 是对的。将 ES6/ES2015 发布到 NPM 没有错,但这可能会引起麻烦,特别是如果使用你的包的人正在使用 Webpack,例如,因为通常人们在使用 [=12= 进行预处理时会忽略 node_modules 文件夹] 出于性能原因。

因此,只需使用 gulpgrunt 或简单地 Node.js 构建一个 lib ES5 文件夹。

这是我的 build-lib.js 脚本,我保存在 ./tools/ 中(这里没有 gulpgrunt):

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));

最后一条建议:您需要将 .npmignore 添加到您的项目。如果 npm publish 没有找到这个文件,它将使用 .gitignore 代替,这会给你带来麻烦,因为通常你的 .gitignore 文件将排除 ./lib 并包含 ./src,这与你发布到 NPM 时想要的完全相反。 .npmignore 文件的语法与 .gitignore (AFAIK) 基本相同。

我喜欢何塞的回答。我注意到有几个模块已经遵循了这种模式。下面介绍如何使用 Babel6 轻松实现它。我在本地安装 babel-cli,这样即使我更改了全局 babel 版本,构建也不会中断。

.npmignore

/src/

.gitignore

/lib/
/node_modules/

安装 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"]
  }
}

如果您想在一个非常简单的小型开源 Node 模块中看到这一点,请查看 nth-day(这是我开始的 - 还有其他贡献者)。查看 package.json 文件和预发布步骤,这将引导您找到执行此操作的位置和方法。如果您克隆该模块,您可以在本地 运行 并将其用作您的模板。

任何人的一些额外注意事项,直接使用来自 github 的 own 模块,而不是通过 published 模块:

(widely used) "prepublish" 挂钩没有为您做任何事情

一个人能做的最好的事情(如果计划依赖 github 回购,而不是发布的东西):

  • unlist src from .npmignore(换句话说:允许)。如果您没有 .npmignore,请记住:将在安装位置使用 .gitignore 的副本,如 ls node_modules/yourProject 所示。
  • 确保,babel-cli 是您模块中的一个依赖项,而不仅仅是一个 devDepenceny,因为您确实是在消费机器上构建,也就是在使用您的模块的应用程序开发人员计算机上
  • install hook 中进行构建,即:

    "install": "babel src -d lib -s"

(尝试任何事情都没有附加值 "preinstall",即可能缺少 babel-cli)

package.json 中的主键决定包发布后的入口点。所以你可以把你的 Babel 的输出放在你想要的任何地方,只需要在 main 键中提到正确的路径。

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

这里有一篇关于如何发布 npm 包的写得很好的文章

https://codeburst.io/publish-your-own-npm-package-ff918698d450

这是您可以参考的示例存储库

https://github.com/flexdinesh/npm-module-boilerplate

TL;DR - 不要,直到 2019 年 10 月。Node.js Modules Team has asked:

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

2019年5月更新

自 2015 年提出此问题以来,JavaScript 对模块的支持已显着成熟,有望在 2019 年 10 月正式稳定。所有其他答案现在已过时或过于复杂。这是当前情况和最佳实践。

ES6 支持

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 支持 99% 的 ES6(又名 2015),版本控制方案现在支持使用年份。

浏览器中的 ES 模块(又名 ECMAScript 模块)

All evergreen browsers have been supporting import-ing ES6 modules since 2017. Dynamic imports are supported 由 Chrome(+ Opera 和 Samsung Internet 等分支)和 Safari。预计下一版本 67 将支持 Firefox。

您不再需要 Webpack/rollup/Parcel 等来加载模块。它们可能仍可用于其他目的,但不需要加载您的代码。您可以直接导入指向 ES 模块代码的 URL。

Node 中的 ES 模块

ES 模块(.mjs 带有 import/export 的文件)自 Node v8.5.0 以来通过使用 --experimental-modules 标志调用 node 得到支持. 2019 年 4 月发布的 Node v12 重写了实验模块支持。最明显的变化是导入时需要默认指定文件扩展名:

// lib.mjs 

export const hello = 'Hello world!';

// index.mjs:

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

注意整个过程中的强制性 .mjs 扩展。 运行 为:

node --experimental-modules index.mjs

Node 12 版本也是模块团队 asked 开发人员不发布 ES 模块包 供 Node.js[=111= 使用] 直到找到通过 require('pkg')import 'pkg' 使用包的解决方案。您仍然可以发布用于浏览器的原生 ES 模块。

原生 ES 模块的生态系统支持

截至 2019 年 5 月,ES 模块的生态系统支持还不成熟。例如,像 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 等)但是,有一种解决方法既适用于 --experimental-modules,也适用于 Babel 转译您的代码,因此您可以使用Jest/Ava/Mocha等:

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

可以说是丑陋的,但是这样你就可以用 import/export 和 运行 和 node --experimental-modules 编写自己的 ES 模块代码,而无需转译器。如果您有尚未准备好 ESM 的依赖项,请按上述方式导入它们,您将能够通过 Babel 使用测试框架和其他工具。


问题的上一个答案 - 请记住,在 Node 解决 require/import 问题之前不要这样做,希望是在 2019 年 10 月左右。

将 ES6 模块发布到 npm,具有向后兼容性

要将 ES 模块发布到 npmjs.org 以便可以直接导入它,而无需 Babel 或其他转译器,只需将 package.json 中的 main 字段指向 .mjs 文件,但省略扩展名:

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

这是唯一的变化。通过省略扩展名,如果 运行 带有 --experimental-modules,Node 将首先查找 mjs 文件。否则它将返回到 .js 文件,所以你现有的 transpilation process 支持旧的 Node 版本将像以前一样工作 — 只需确保将 Babel 指向 .mjs 文件。

这是我发布到 NPM 的 source for a native ES module with backwards compatibility for Node < 8.5.0。您现在就可以使用它,无需 Babel 或其他任何东西。

安装模块:

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

创建测试文件test.mjs:

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

运行 节点 (v8.5.0+) 带有 --experimental-modules 标志:

node --experimental-modules test.mjs

TypeScript

如果使用 TypeScript 开发,可以生成 ES6 代码并使用 ES6 模块:

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

然后,您需要将 *.js 输出重命名为 .mjs,一个已知的 issue 希望很快得到修复,以便 tsc 可以输出 .mjs直接文件。

根据您模块的结构,此解决方案可能行不通,但如果您的模块包含在单个文件中,并且没有依赖项(不使用import),使用以下模式,您可以按原样发布代码,并且可以使用 import(浏览器 ES6 模块)导入和 require(Node CommonJS 模块)

作为奖励,它适合使用 SCRIPT HTML 元素导入。

main.js :

(function(){
    'use strict';
    const myModule = {
        helloWorld : function(){ console.log('Hello World!' )} 
    };

    // if running in NODE export module using NODEJS syntax
    if(typeof module !== 'undefined') module.exports = myModule ;
    // if running in Browser, set as a global variable.
    else window.myModule = myModule ;
})()

my-module.js :

    // import main.js (it will declare your Object in the global scope)
    import './main.js';
    // get a copy of your module object reference
    let _myModule = window.myModule;
    // delete the the reference from the global object
    delete window.myModule;
    // export it!
    export {_myModule as myModule};

package.json:`

    {
        "name" : "my-module", // set module name
        "main": "main.js",  // set entry point
        /* ...other package.json stuff here */
    }

要使用您的模块,您现在可以使用常规语法...

NODE 中导入时 ...

    let myModule = require('my-module');
    myModule.helloWorld();
    // outputs 'Hello World!'

BROWSER 中导入时 ...

    import {myModule} from './my-module.js';
    myModule.helloWorld();
    // outputs 'Hello World!'

甚至使用 HTML 脚本元素...

<script src="./main.js"></script>
<script>
     myModule.helloWorld();
    // outputs 'Hello World!'
</script>

按照 José 和 Marius 的方法,(2019 年更新了 Babel 的最新版本):将最新的 JavaScript 文件保存在 src 目录中,并使用 npm 的 prepublish 脚本构建并输出到lib目录。

.npmignore

/src

.gitignore

/lib
/node_modules

安装 Babel(我的版本是 7.5.5)

$ 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"
    ]
  }
}

我有 src/index.js 使用箭头函数:

"use strict";

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

这里是the repo on GitHub.

现在您可以发布包了:

$ npm publish
...
> latest-js-to-npm@1.0.0 prepublish .
> babel src -d lib

Successfully compiled 1 file with Babel.
...

在发布包到npm之前,你会看到已经生成了lib/index.js,转译到es5:

"use strict";

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

NewOneWithParameters(10, 20);

[Rollup 捆绑器更新]

正如@kyw 所问,您将如何集成 Rollup 打包器?

首先,安装rolluprollup-plugin-babel

npm install -D rollup rollup-plugin-babel

其次,在项目根目录下创建rollup.config.js

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/**"
    })
  ]
};

最后,在 package.json

中更新 prepublish
{
  ...
  "scripts": {
    "prepublish": "rollup -c"
  },
  ...
}

现在可以运行 npm publish,在package发布到npm之前,你会看到生成了lib/index.js,转译成es5:

'use strict';

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

NewOneWithParameters(10, 20);

注意:顺便说一下,如果您使用的是 Rollup 打包器,则不再需要 @babel/cli。您可以安全地卸载它:

npm uninstall @babel/cli

Node.js 13.2.0+ 支持没有实验标志的 ESM,并且有几个选项可以发布混合(ESM 和 CommonJS)NPM 包(取决于所需的向后兼容性级别):https://2ality.com/2019/10/hybrid-npm-packages.html

我建议采用完全向后兼容的方式来使您的包的使用更容易。这可能如下所示:

混合包有以下文件:

mypkg/
  package.json
  esm/
    entry.js
  commonjs/
    package.json
    entry.js

mypkg/package.json

{
  "type": "module",
  "main": "./commonjs/entry.js",
  "exports": {
    "./esm": "./esm/entry.js"
  },
  "module": "./esm/entry.js",
  ···
}

mypkg/commonjs/package.json

{
  "type": "commonjs"
}

从 CommonJS 导入:

const {x} = require('mypkg');

从 ESM 导入:

import {x} from 'mypkg/esm';

我们做了一个 investigation into ESM support in 05.2019,发现很多库缺乏支持(因此建议向后兼容):