建议
🔍 搜索词
双 CommonJS / CJS + ES module / ESM 包
✅ 可实现性检查清单
我的建议符合以下准则:
- 这不会对现有的 TypeScript/JavaScript 代码造成破坏性的改变
- 这不会改变现有 JavaScript 代码的运行时行为
- 这可以在不根据表达式的类型生成不同的 JS 的情况下实现
- 这不是一个运行时特性(例如库功能、带有 JavaScript 输出的非 ECMAScript 语法、新的 JS 语法糖等)
- 这个特性将与 TypeScript's Design Goals 的其他部分保持一致。
最后一个复选框未勾选,因为这个功能请求是关于扩展 tsc 在构建管道中的作用,并且它是关于与 Node 的行为保持一致,而不是 ECMAScript 的行为,后者没有包的概念。话虽如此,Node 是 TypeScript 的两个主要目标之一(另一个是 Web)。
⭐ 建议
这个建议更抽象,而不是一个精确的实现提案。问题在于,为 Node 编写双 CJS/ESM 包目前无论是有还是没有 TypeScript,都存在相当大的摩擦,而 tsc 可以做得更好。我理解如果这个问题被认为是超出范围并关闭的话。
我在写这个假设阅读此内容的人了解 Node 16 中的 ESM 以及它们的约束条件。
使用 "module": "nodenext"
,TypeScript 通过使用带有 .js 扩展名的 import specifiers 使 .ts 文件可以导入其他 .ts 文件,以便浏览器和 Node 可以运行生成的代码。也就是说,当 tsc 看到 import './x.js';
时,它知道要查找 ./x.ts 以解析模块。这样,tsc 就不需要重写 import specifier 并可以在编译后的 JS 代码中发出 import './x.js';
。
单独地,双 CJS/ESM 包需要它们的一些文件使用 .mjs 或 .cjs 文件扩展名。这是因为包声明了 "type": "module"
或者 "type": "commonjs"
,而其他类型的文件需要分别使用 .cjs 或 .mjs。(顺便说一下,这个问题无论包是否使用 TypeScript都存在。即使它们没有使用 TypeScript,编写 ESM 作者也需要复制或编译他们的 ESM 以生成 CJS。)
这个功能请求是让 tsc 能够发出带有 .js 文件扩展名重写的编译后的 JS 以及带有类似重写的 import specifiers。这样一来,包的源代码就是它的 .ts 文件,运行例如 tsc && tsc -p tsconfig.commonjs.json
将为双模式包生成 ESM 和 CJS。
📃 有动力的例子
考虑一个由多个具有内部包导入的文件组成的包。
hello-world.ts
import b from './b.js';
export default function helloWorld() { return `${b()} world`; }
hello.ts
export default function hello() { return `hello`; }
作者希望发布该包的 CJS 和 ESM:
package.json
{
"type": "module",
"//": "Using longhand notation below for clarity"
"exports": {
".": {
"import": "./build/esm/hello-world.js",
"require": "./build/cjs/hello-world.cjs",
"types": "./build/esm/hello-world.d.ts"
}
}
}
即使涉及两个 tsconfig.json 文件(例如 tsconfig.json 和 commonjs.json,后者扩展前者并覆盖一些选项),能够运行两次 tsc 以生成可用的 ESM 和 CJS 将是有用的。换句话说,在这个例子中,发出的 CJS 需要 .cjs 文件扩展名和带有 cjs import specifiers,因为 package.json 声明了 "type": "module"
。
8条答案
按热度按时间thtygnil1#
我正在写这篇文章,假设阅读这篇文章的人了解Node 16中的ESM工作及其约束。
我认为你的受众可能比你想象的要小得多🙃
使用
"module": "nodenext"
,TypeScript允许.ts
文件通过使用具有.js
扩展名的import specifiers来导入其他.ts
文件,以便浏览器和Node可以运行生成的代码。也就是说,当tsc看到import './x.js';
时,它知道要查找./x.ts
来解析模块。这样,tsc不需要重写import specifier,并可以在编译后的JS代码中发出import './x.js';
。重要提示:
--module
模式下都是如此,只是在nodenext
中强制执行,因为在那里我们可以自信地说其他任何事情都是错误的。所以我认为这个
这个功能请求是让tsc能够发出带有.js文件扩展名重写的已编译JS
是我们不打算涉足的领域。
然而,我认为这个问题是我们应该意识到并有一些指导方针的问题,并理解现有的工具。我的理解是这是Rollup的主要用途之一。你尝试过吗?体验如何?
我还知道@weswigham对此有一些特定的建议(即创建一个最小化的ESM Package 器,围绕CJS源真相,以及考虑是否真的需要特定的ESM入口点)。
zengzsys2#
这是在每个模块模式下都是真实的,只是在nodenext中被强制执行,因为我们可以自信地说其他任何事情都是错误的。
我不知道这个,很有帮助。
说实话,在接触到ESM/CJS情况的细节后,我理解这些是TypeScript团队不得不面对的难题。一个包是否使用TypeScript,它需要一些构建过程来从ESM文件派生CJS文件,以便为在ESM中编写的双模式包提供支持。
考虑如何编写一个没有TypeScript的包。我想用原生ESM编写代码,期望它是JS生态系统的发展方向。我也想让我的包在CJS和ESM项目中都能工作,直到我的包消费者中有足够多的人转向ESM,因此制作了一个双模式包。据我所知,主要源代码用ESM编写的双模式包不能使用CJS Package 器,因为CJS只能异步地将ESM转换为CJS。因此,这类双模式包需要将ESM转换为CJS(并重写.js
import
s为.cjsrequire()
s),并发布ESM和CJS版本的代码。这与TypeScript完全无关,如果我是TypeScript团队的话,我会认为这是超出范围的。可以说,将事物建模的方式是tsc负责将TS编译为ESM,问题简化为上述情况:在非TypeScript项目中使用任何工具将ESM编译为CJS。
我没有尝试过Rollup。我的观点是,我希望只有一个工具来处理这个问题。我怀疑一年后有了像Bun或Rome这样的东西会更好,祝好运。
bttbmeg03#
我知道你写过,你想用一个工具(tsc)来处理这个问题,但也许https://github.com/AlCalzone/esm2cjs可以解决你当前的问题。这是我在遇到同样问题时创建的一个小型 Package 器——让tsc输出ESM,并以某种方式将其转换为混合的ESM和CJS包。
lfapxunr4#
我认为Rollup(或者AlCalzone的工具)就像Bun或Rome一样,是“一个工具”,除非你认为罗马也会取代TypeScript的类型检查器。但我认为这些工具的大部分想法是tsc只进行类型检查,而其他东西负责产生输出,无论是简单的逐文件转换还是任何数量的转换和捆绑在一起。
wwwo4jvm5#
如果在
"module": "node16"
或"module": "nodenext"
中,TypeScript编译器可以根据package.json
中的"module"设置输出ESM和CommonJS,而不是仅输出其中一个,那就太好了。nwlqm0z16#
我认为
4.7
犯了一个小错误,即如果tsc
具有module: nodenext, outputDir: dist/cjs
,其中dist/cjs
具有一个带有type: commonjs
的package.json
,但根package.json
是type: module
,输出将是esm
...yqhsw0fo7#
更现代的方法是使用
package.json
中的新"exports"属性来发布双ESM/CommonJS包。(为了获得最广泛的支持,您可能还需要在顶部指定module
和/或browser
,支持程度差异很大)i7uaboj48#
我原本打算创建一个关于更好支持混合式发射的问题,但现在已经有了。
我在 https://kuhrt.me/logs/hybrid-esm-cjs-node-packages-using-typescript 处记下了我的处理方法。
我为 ESM 编写了库,然后要通过构建脚本将所有文件转换为
.cjs
格式,并相应地更改文件中的导入路径。这感觉像是一个很大的技巧,TypeScript 应该以某种方式支持这种模式。