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

Posted

技术标签:

【中文标题】如何将用 ES6 编写的模块发布到 NPM?【英文标题】:How to publish a module written in ES6 to NPM? 【发布时间】:2015-06-26 14:49:11 【问题描述】:

我正要向 NPM 发布一个模块,当时我考虑用 ES6 重写它,以使其既能适应未来,又能学习 ES6。我使用 Babel 转译为 ES5,并运行测试。但我不确定如何继续:

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

简而言之:我需要采取哪些步骤才能将一个用 ES6 编写的模块发布到 NPM,同时仍然允许人们浏览/分叉原始代码?

【问题讨论】:

我最近一直在为这个决定而苦苦挣扎。我看到José 标记为正确的答案也是共识。 这是我的2018 answer,考虑到自 2015 年以来模块支持的进展。 如果我能反其道而行之,我会很高兴。使用 ES Module 导入 NPM 模块,但这些是我得到的唯一结果。 【参考方案1】:

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,发现很多库都缺乏支持(因此建议向后兼容):

esm 包的支持与 Node 不一致,这会导致问题 “内置要求无法旁加载 .mjs 文件。” https://github.com/standard-things/esm#loading, https://github.com/standard-things/esm/issues/498#issuecomment-403496745 “.mjs 文件扩展名不应该是开发人员想要互操作或易于使用的东西。它是可用的,因为它在 --experimental-modules 中,但由于它没有完全出炉,我不能承诺任何增强给它。” https://github.com/standard-things/esm/issues/498#issuecomment-403655466 mocha doesn't have native support for .mjs files 2020-01-13 更新:Mocha 在mocha@7.0.0-esm1 发布了实验性支持 许多备受瞩目的项目都存在.mjs 文件的问题: create-react-app react-apollo graphql-js inferno

【讨论】:

我似乎无法在 node_modules 文件夹中导入全局安装的 ES6 模块(由 npm root -g 提供)。难道我们真的不应该这样做吗?我真的很困惑。我知道 npm link 可以通过将模块链接到我的本地 node_modules 文件夹来解决问题,但我想知道为什么不支持导入全局节点模块。 回答我自己,我想它永远不会被支持:github.com/nodejs/node-eps/blob/master/… 虽然这是一个非常愚蠢的决定,但很容易支持...... @JoakimL.Christiansen Namaste。由于节点社区对模块的完全错误的官方立场,我遇到了类似的困难,我关注了那个链接,是的,我很失望,但不满意,我检查了那个链接文章的发布日期...... 8月9日2017. 只是指出链接的文章可能是一个红鲱鱼可以这么说......【参考方案2】:

我喜欢何塞的回答。我已经注意到几个模块已经遵循了这种模式。这是您可以使用 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"]
  

【讨论】:

scripts 中的任何命令都会将node_modules/.bin 添加到它们的$PATH 中,并且由于babel-cli 将二进制文件安装到node_modules/.bin/babel,因此无需通过路径引用该命令。 请注意prepublish 是有问题的,因为它可以在安装时运行(github.com/npm/npm/issues/3059),更喜欢更惯用的version 脚本挂钩(docs.npmjs.com/cli/version) @mattecapu 似乎prepublish 的问题仍然存在。目前我认为手动编译src目录和npm publish是要走的路。 您可以使用prepublishOnly 脚本挂钩(请参阅docs.npmjs.com/misc/scripts#prepublish-and-prepare)。请注意,在 npm 的第 5 版中,这应该可以按预期运行,但现在(假设您使用的是 npm v4+)这应该可以工作。 @FrankNocke prepublishpublish (obv.) 之前运行,它将内容推送到 npm(或您配置的任何位置)。所以它用于构建 NPM 包中的内容,即使它没有被签入。【参考方案3】:

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

[汇总捆绑器的更新]

正如@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/**"
    )
  ]
;

最后,将prepublish 更新为package.json


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

现在你可以运行npm publish,在包发布到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

【讨论】:

如何集成 Rollup 捆绑器? @kyw,关于如何集成 Rollup 捆绑器,请参阅我的更新答案。 2019 年 12 月更新 --> github.com/rollup/rollup/blob/… 谢谢!澄清一下,要将您的功能集成到另一个项目中,您可以使用 import NewOneWithParameters from 'latest-js-to-npm'; 吗?所以在你的代码中你实际上不需要export?【参考方案4】:

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

请不要在 [2019 年 10 月] 之前发布任何打算供 Node.js 使用的 ES 模块包

2019 年 5 月更新

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

ES6 支持

Node since version 6 支持 99% 的 ES6(又名 2015)。当前的 Node 版本是 12。所有常青浏览器都支持绝大多数 ES6 功能。 ECMAScript 现在位于 version 2019,版本控制方案现在支持使用年份。

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

All evergreen browsers 自 2017 年以来一直是 supporting import-ing ES6 模块。Dynamic imports 是 Chrome(+ Opera 和 Samsung Internet 等分支)和 Safari 的 supported。下一个版本 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 开发人员在找到解决方案之前不发布 供 Node.js 使用的ES 模块包通过require('pkg')import 'pkg' 使用包。您仍然可以发布用于浏览器的原生 ES 模块。

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

截至 2019 年 5 月,对 ES 模块的生态系统支持还不成熟。例如,Jest 和 Ava 等测试框架不支持 --experimental-modules。您需要使用转译器,然后必须在使用命名导入 (import symbol ) 语法(目前不适用于大多数 npm 包)和默认导入语法 (import Package from 'package') 之间做出决定,这确实有效,但是not when Babel parses it 用于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 编写自己的ES 模块代码并用node --experimental-modules 运行它,而无需转译器。如果您有尚未 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 文件,因此您现有的支持旧 Node 版本的 transpilation process 将像以前一样工作 — 只需确保将 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...');

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

node --experimental-modules test.mjs

打字稿

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

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

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

【讨论】:

说“所有常青浏览器都支持绝大多数 ES6 特性。”当您查看 data 并意识到浏览器中的 es6 支持仅达到所有用户的 80% 左右时,这并不意味着什么。 目前这个生态系统肯定还不够成熟。发布 v12 的 Node.js 团队特别要求:“在解决此问题之前,请不要发布任何打算供 Node.js 使用的 ES 模块包。” 2ality.com/2019/04/nodejs-esm-impl.html#es-modules-on-npm Mocha 本身不支持 .mjs 文件。多多库(例如 create-react-app、react-apollo、graphql-js)在包含 mjs 文件的依赖项方面存在问题。 Node.js 计划在 2019 年 10 月推出官方支持,这是我最早会认真考虑的。 @thisismydesign:感谢您提醒更新这个旧答案!刚刚完成。 “所有其他答案现在都已过时或过于复杂。这是当前情况和最佳实践。”这似乎非常主观,绝对不能反映 2021 年发布的 npm 包的状态。【参考方案5】:

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

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

ma​​in.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>

【讨论】:

【参考方案6】:

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

【讨论】:

那篇文章忽略了这样一个事实,即您可以(并且应该)直接使用 publish to NPM modules authored in ES6 和 importable 而无需 Babel 或任何其他转译器。【参考方案7】:

对任何人的一些额外说明,直接从 github 使用 自己的 模块,而不是通过 已发布 模块:

(widely used)“prepublish”钩子并没有为你做任何任何事情

可以做的最好的事情(如果计划依赖 github repos,而不是发布的东西):

从 .npmignore 中取消列出 src(换句话说:允许它)。如果您没有.npmignore,请记住:将在安装位置使用.gitignore 的副本,因为ls node_modules/yourProject 会显示给您。 请确保,babel-cli 是您模块中的依赖项,而不仅仅是 devDepenceny,因为您确实是在使用您的模块的应用程序开发人员计算机上的消费机器上构建的

在install hook 中进行构建,即:

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

(尝试任何“预安装”都没有附加价值,即可能缺少 babel-cli)

【讨论】:

安装时编译非常粗鲁。请不要这样做 - npm 安装时间已经够糟糕了!对于您希望避免使用 npm 包存储库的内部代码,您可以:1) 使用 monorepo,2) 上传并依赖 npm pack 输出,3) 签入构建输出。【参考方案8】:

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

【讨论】:

【参考方案9】:

@Jose 是对的。将 ES6/ES2015 发布到 NPM 并没有什么问题,但这可能会导致麻烦,特别是如果使用你的包的人正在使用 Webpack,例如,因为通常人们在使用 babel 进行预处理时会忽略 node_modules 文件夹,因为性能原因。

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

这是我的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) 基本相同。

【讨论】:

您可以使用files field in package.json 而不是.npmignore。它可以让您准确指定要发布的文件,而不是寻找您不想想要发布的随机文件。 这不会破坏摇树吗? 我不推荐使用.npmignore,改用package.jsonfiles,见:github.com/c-hive/guides/blob/master/js/… @thisismydesign:这正是我在上面的评论中所推荐的......? 我的错,没注意到 :)【参考方案10】:

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

如果你满足这两个要求,你可以做任何你想做的事。 即使模块是用 ES6 编写的,如果最终用户不需要知道这一点,我现在会对其进行转换以获得最大的支持。

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

外卖

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

【讨论】:

这似乎没有回答这个问题。我认为 OP 正试图弄清楚如何构建他们的 Github 存储库以及发布到 NPM 的内容,而您所说的只是他们可以做任何他们想做的事情。 OP 希望就这种情况的良好做法提出具体建议。 @jfriend00 我不同意。我建议他转译,只发布require( 'package' ) 工作所需的文件。我将编辑我的答案以使其更清楚。也就是说,何塞的回答比我自己的要好得多。 José 的回答非常好,但我很欣赏这个回答,因为它明确概述了何时/为什么使用一个包和两个包的良好经验法则。【参考方案11】:

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

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

【讨论】:

你有示例存储库吗? @JamesAkwuh 请注意,您可能希望更改 package.json 中的“start”和“build”命令以使用 babel-cli 的相对路径:./node_modules/babel-cli/bin/babel.js -s inline -d lib -w src。这应确保在部署到新环境时安装不会失败。 @phazonNinja npm 处理它 “如果没有 .npmignore 文件,但有 .gitignore 文件,则 npm 将忽略 .gitignore 文件匹配的内容。” official npm docs 您可以使用files field in package.json 而不是.npmignore。它可以让您准确指定要发布的文件,而不是寻找您不想想要发布的随机文件。

以上是关于如何将用 ES6 编写的模块发布到 NPM?的主要内容,如果未能解决你的问题,请参考以下文章

如何将 ES6 导入与“请求”npm 模块一起使用

如何判断一个特定模块是 CommonJS 模块还是 ES6 模块?

访问JSX文件中的非es6 npm模块函数

如何使用 npm 在 ES6 中导入 moment.js?

我们如何或可以通过 npm 和 Meteor 使用节点模块?

如何让 webpack treeshake 我的 ES6 模块?