You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
src/path/to/file.ts:1:31 - error TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("my-npm-module")' call instead.
To convert this file to an ECMAScript module, change its file extension to '.mts', or add the field `"type": "module"` to 'package.json'.
TS2507: Type 'typeof import("/node_modules/eventemitter3/index")' is not a constructor function type.
虽然这个改成 import { EventEmitter } form 'eventemitter3' 就没问题了,但另一个模块怎么改都不行:
import userEvent from '@testing-library/user-event'
// 这里也会报同样的错误
// TS2339: Property 'click' does not exist on type 'typeof import("/node_modules/@testing-library/user-event/dist/index")'.
userEvent.click()
webpack 的控制台报了很奇怪的 warning:export 'myMethod' (imported as 'myMethod') was not found in 'my-npm-module' (possible exports: __esModule),但是我确认我的模块是有定义 myMethod 的:exports.myMethod = ...
这个问题没什么头绪,然后我就发现浏览器控制台也报了错:exports is not defined,报错的代码为 exports.getToken = void 0。
自从 Node.js 原生支持 es module 之后,越来越多的 npm 包开始切换为纯 es module,即只提供 es module 的代码,然后由消费端来做处理,比如:
如果想要在不支持 es module 的 Node.js 里使用纯 es module,需要用 import(),例如 import('my-npm-module')
如果想要在浏览器环境使用纯 es module,那么需要使用 Webpack 这类代码打包器
如果用了 typescript,那么 typescript 就会做一些检查,比如:
如果你用了 import 'my-npm-module' 的形式且 my-npm-module 是个纯 es module,而你自己的项目又是 CommonJS(即没有在 package.json 里声明 "type": "module" 或没有用 .mts),它就会认为你是在 CommonJS 里 import es module,就会报错 TS1479: The current file is a CommonJS module whose imports will produce 'require' calls ...
但如果你把你自己的项目也声明成 es module,那么就需要做前面那些改造,比如加上 .js 后缀之类的
更新:TypeScript 5.0 给 moduleResolution 添加了一个新的选项
bundler
,可以解决文中提到的需要给输出的文件加上后缀之类的问题。我个人还是倾向于严格遵守 ES 标准,但这个新选项可以用于快速绕过 ES 标准的要求,很适合那些想要暂时专注于开发业务代码、不想花时间在兼容 ES 标准上的情况。
相关说明:https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#moduleresolution-bundler
我准备将我写的一些 NPM 包改为直接用 tsc 来输出,原因见 开发 NPM 包时可能不需要将代码打包进一个文件里
对 build 命令进行改造
先介绍一下我要改造的第一个 NPM 包,它的 package.json 在改造前是这样的:
为了让 typescript 支持 package.json 里的
exports
,所以我在 tsconfig.json 里配置了"moduleResolution": "NodeNext"
(原本是Node
);再加上我在 tsconfig.json 里配置了"module": "ESNext"
,所以 typescript 输出的文件都会是 es module,所以我提前在 package.json 里配置了"type": "module"
。我先是改造了 build 命令,改为了
"build": "tsc"
,试着运行了下,报了第一个错,是关于文件后缀的。我在代码中使用了下面的 import:
前面提到,我为了让 typescript 支持 package.json 的 exports,所以设置了
"moduleResolution": "NodeNext"
,而这就要求导入模块时带上文件后缀。加上 '.js' 后缀(只能是 '.js',其它后缀比如 '.ts' 都会报错)之后,build 命令就能成功运行了。运行之后,代码输出到了
dist
文件夹,且都是单个的文件,没有打包成一个文件。然后,package.json 就成了这样:
几点变化:
type: module
,因为我输出的 .js 文件使用的是 es module 格式types
。因为声明文件就叫index.d.ts
且跟index.js
处于同一个文件夹下,所以 ts 默认就会读取index.d.ts
,不需要额外声明,当然,声明了也没问题。遇到的第一个问题:给 import 带上
.js
后缀后,Jest 报Can't find module
错误目前为止看起来不错,但运行
jest
时出了问题:我还特意试了下,去掉 .js 后缀就能成功运行,加上就不行了。
出错的原因在于,虽然代码里写的是
utils.js
,但文件实际上是utils.ts
。第一个解决方案:用 jest 的
moduleNameMapper
谷歌了一下,发现给 jest config 加上
moduleNameMap
可以解决这个问题(解决方案来自这里):但如果有很多个文件,总不能一个个加,所以我尝试用正则来匹配:
但这样做又报错了,因为影响到了 node_modules 里的文件,但是我试了很多次也没法做到只匹配到我自己的文件。
第二个解决方案:先 build,再运行 jest
先运行 build 把 index.ts 和 index.test.ts 都输出成 js 文件,然后再运行 jest 跑 index.test.js 就没问题了。
另外还要给 jest config 添加
testMatch: ['<rootDir>/dist/**/*.test.js']
感觉有点繁琐。
第三个解决方案:
"moduleResolution": "NodeNext"
改为"moduleResolution": "Node"
这样写代码时就不用带后缀了,但这样的话,输出的代码里也是没带后缀的,如果直接在 Node.js 里 import 会报错,因为 import 是要求带后缀的。在 Webpack 里也是会报错的,除非设置
fullySpecified: false
。但还是应该按照标准来。
第四个解决方案:安装一个 npm 包
换了一下谷歌搜索关键词为
jest typescript with extension
,找到了一个相关的 issue:kulshekhar/ts-jest#1057但其实这个问题跟 ts-jest 没关系,这是 jest 的 jest-resolve 的问题,然后 issue 里有人开发了一个 npm 包来解决这个问题,见 https://github.com/VitorLuizC/ts-jest-resolver
第五个解决方案:还是 moduleNameMapper
还是在刚才的 issue 里找到的解决方案。如果你不想单独安装一个 npm 包,可以添加 moduleNameMapper
来源 swc-project/jest#64 (comment)
但是我个人比较喜欢 npm 包,因为这应该是 resolver 负责解决的问题。moduleNameMapper 是被设计用来把一些对 css、image 之类的文件的 import 转为 js 模块的。
小结
搜索关键词很重要 😂
遇到的第二个问题:tsc 把 *.test.ts 也输出了
写在 src 目录下的 index.test.ts 也被输出到了 dist/index.test.js,这会导致 jest 同时运行了 dist/index.test.js 和 src/index.test.ts。就算不是因为这个,输出文件夹内也不应该包含测试文件。
这个问题只能单独创建一个 build 用的 tsconfig 把测试文件排除掉了:
然后把 build 命令改为
tsc -p tsconfig.build.json
对消费端(即使用我们的 NPM 包的项目)的改造
前面都是对 NPM 包本身进行改造的,但是我们的 NPM 包最终会用在别的项目(下文简称“消费端”)里。在这么改造之后,消费端也需要做一些调整。
以划词翻译为例。
划词翻译的项目使用了 Webpack + babel-loader 来编译,tsconfig.json 里也设置了
"moduleResolution": "NodeNext"
。当 build 的时候,typescript 会报第一个错误:我没有在划词翻译的 package.json 里设置
"type": "module"
,所以项目里的 .ts 文件默认会被当作 CommonJS,而 CommonJS 里的 require() 方法是不能用来导入 es module 的。但是,作为一个 Webpack 项目,我不需要让 typescript 认为我会输出成 es module,因为它在我的 Webpack 项目中只负责进行类型检查,真正负责文件输出的是 Webpack,负责处理 es module 的也是 Webpack,即使我在 ts 文件里直接 import es module,Webpack 也是能处理的。
所以第一步我首先尝试将 tsconfig.json 里的
moduleResolution
从 'NodeNext' 改为了 'Node'。这么改了之后就不会报 TS1479 错误了,因为
moduleResolution: Node
会忽略(或者用“不支持”更准确一些) package.json 里的type
和exports
字段,所以 npm 报默认就会被当作是 CommonJS,但也正因为它忽略了exports
,就有了第二个报错:前面我提到,我改造完成后的 npm 包 package.json 大概长这样:
但是,只有当 tsconfig.json 里的
moduleResolution
为 'NodeNext' 时,typescript 才会识别exports
字段。而当 moduleResolution 是Node
时,只会识别传统的main
、types
字段,所以这里要给 NPM 包的 package.json 单独加上"types": "./dist/index.d.ts"
虽然解决了问题,但我又有了一个疑惑:如果我的 NPM 包有 submodule 会如何?
比如如果我的 npm 包的 package.json 是下面这样:
types 只能给 index.js 声明类型,然后我试了一下
import 'my-npm-module/submodule'
,果然 typescript 就报错了:在 package.json 里没有 exports 之前,这类 submodule 是根据路径来的,然后我试了下在 my-npm-module 的 package.json 旁边创建了一个
submodule.d.ts
文件,果然 typescript 就没有报错了。那么现在,我面临一个选择。
如果我想要兼容
"moduleResolution": "Node"
或者不支持 package.json exports 的 Node.js(比如 v12 以下的版本)那么我就需要做一些额外的配置,比如将 submodule 输出到根目录下,就像这样:
这种就是同时兼容旧版本和新版本的,但这样的话就得把 tsconfig.json 的
outDir: 'dist'
改为outDir: '.'
,这样一来文件都被直接输出在根目录下,看上去就很混乱。又或者保持目录结构不变,但引用路径里多一层
dist
目录,就像这样:又或者单独为 entry 在根目录下创建转发 export 的文件,比如这样:
这样的话 package.json 就可以保持原样、import 路径里也不用加入
dist
目录了。但是无论哪种都需要额外的步骤,比较繁琐。
如果我不想做兼容
那就很快乐了:
"moduleResolution": "NodeNext"
,当然了,这样一来消费端要进行相应的改造我的选择
我选择在消费端做改造。现在的 Node.js、Webpack、TypeScript 都已经支持 package.json 的 exports 了,没必要再单独对旧版本做兼容。
但是要注意:对消费端的改造看起来是似乎是由于
"moduleResolution": "NodeNext"
造成的,但实际上不是的。这个选项仅仅只是让 typescirpt 支持识别 package.json 的新字段如 type 和 exports,真正导致消费端需要做改造的原因是因为,前面例子里的 my-npm-module 是一个纯 es module。换句话说,如果 my-npm-module 是一个纯 commonjs 模块,那么即使消费端是
"moduleResolution": "NodeNext"
也没有任何问题。或者,也可以在提供 es module 的同时也提供 commonjs,就像这样:
关于如何使用 typescript 同时提供两种格式的输出文件,可以参考 https://styfle.dev/blog/es6-modules-today-with-typescript
但实际操作时我发现了一个问题,那就是虽然文件改成了 .mjs,但是代码里的 import 语句没有带上完整的文件扩展名,比如
import './utils'
应该是import './utils.mjs'
才对,如果不加的话,在 Webpack 里倒是没关系,但在 Node.js 里使用就会报错。新的
.mts
可能可以满足同时提供 mjs / cjs 的场景,从网上搜到的一篇文章 Publish ESM and CJS in a single package 也提到了两个工具可以用一份 ts 同时生成 mjs 和 cjs,不过我还是以后再踩坑吧。最后我自己写了个模块来处理 js -> mjs 的转换 😂
https://www.npmjs.com/package/@hcfy/js-to-mjs
开始实施
所以总结下来,消费端的改造有以下两种方法,二选一:
"moduleResolution": "Node"
,然后确保所有 npm 包都是使用main
、types
字段来提供输出文件的路径的,如果有 submodule 那么引用方式要匹配文件路径(即import 'my-module/submodule'
要确保 my-module 目录下有 submodule.js 文件存在)"moduleResolution": "NodeNext"
。但这样一来如果 npm 包里有 es module(即 "type": "module")那么 typescript 就会报错。如果你确认你的代码不是用在 Node.js 的 CommonJS 环境的(比如前端项目里使用 Webpack 打包代码),那么这个错就仅仅是 typescript 的错,不会影响到代码打包,要解决的话,以下方法三选一:再次强调,使用 @ts-ignore 的方式只能在你确认你的代码不是运行在 Node.js 的 CommonJS 环境的,因为 CommonJS 环境是不能 require 一个 es module
require('my-module')
的,要么使用import('my-module')
,要么把自己的代码也改成 es module 的。把消费端整个改为
"type": "module"
的坑我一开始选择的是把消费端整个改为 es module 的,但有不少问题。
第一个问题,当我把 '.js' 后缀加上后,webpack 会报
Can't resolve module
的错误,这感觉就跟 jest 一样,显式指定了 .js 所以 webpack 就真的去读取 .js 了。既然是跟 module resolve 有关的报错,我就翻了一下 webpack resolve 的配置项,果然就让我找到了一个 resolve.extensionAlias,加上下面的配置就不会报错了(但我感觉加了这个之后 build 变慢了,不知道是不是错觉):
然后又遇到了第二个问题,typescript 解析模块的类型时出问题了,比如这样的代码:
没给 package.json 加 "type": "module" 之前是没问题的,但加了之后在
extends EventEmitter
那里出现问题了:虽然这个改成
import { EventEmitter } form 'eventemitter3'
就没问题了,但另一个模块怎么改都不行:我检查过它们的 d.ts,它们都有使用
export default
导出类型,我感觉当声明了"type": "module"
之后,typescript 对export default
的导出有问题。在遇到这两个问题之后,我决定不折腾了。all in esmodule 并没有为我带来好处,反而有很多坑,而我一开始的初衷只是不想把 npm 包打包成一个文件而已,所以我决定把我自己的 npm 包全输出成 commonjs 形式,这样消费端也不用改了,jest 也不用单独安装一个 resolver 了、也不用强制带上 .js 后缀了。
所以我踩了一天的坑,而这些坑都是因为我使用了
"type": "module"
导致的,如果一开始就选择 CommonJS,那么啥事都没有 😂把 npm 包全改为 commonjs 输出后的坑
然后,我把我自己的所有 npm 包都输出成了 commonjs 的形式,但是在消费端的 webpack 打包的时候出现了一些异常:
export 'myMethod' (imported as 'myMethod') was not found in 'my-npm-module' (possible exports: __esModule)
,但是我确认我的模块是有定义myMethod
的:exports.myMethod = ...
这个问题没什么头绪,然后我就发现浏览器控制台也报了错:
exports is not defined
,报错的代码为exports.getToken = void 0
。这个错也很奇怪,看了一下 webpack 打包出来的代码,包裹这段代码的形式是这样的:
奇怪的地方就在于
__webpack_exports__
,它应该就是exports
才对。网上查了一下,有人提到是 @babel/transform-runtime 导致的,然后我联想到我给 babel env 用了
useBuiltIns: 'usage'
,然后我注释掉之后又试了下,上面两个问题就都没有了,但同时,corejs 引入的代码也没有了。看起来这个问题出现的整个流程是这样的:
useBuiltIns: 'usage'
,这就导致我的 npm 包被插入了一堆 corejs 引入的代码exports
变成了__webpack_exports__
所以解决这个问题有两个方案:
useBuiltIns: 'usage'
,改为使用useBuiltIns: 'entry'
,在 entry 开头插入代码useBuiltIns: 'usage'
插入进来的代码不要影响到我的模块类型。一个可能有用的链接:https://stackoverflow.com/questions/52407499/how-do-i-use-babels-usebuiltins-usage-option-on-the-vendors-bundle按照链接里的说明将 babel config 的 sourceType 改成 'unambiguous' 果然就没问题了,顺便看了下 babel 关于 sourceType 的说明,确实就提到了对 node_modules 下的文件进行处理时会导致这个问题。
在改造前之所以没有这个问题,是因为改造前我提供了 es module 的输出文件,所以用 import 语句插入 corejs 是没有问题的;但现在我是纯 commonjs 了,还用 import 语句就会出现这个问题。
关于纯 es module
自从 Node.js 原生支持 es module 之后,越来越多的 npm 包开始切换为纯 es module,即只提供 es module 的代码,然后由消费端来做处理,比如:
import()
,例如import('my-npm-module')
"type": "module"
或没有用.mts
),它就会认为你是在 CommonJS 里 import es module,就会报错TS1479: The current file is a CommonJS module whose imports will produce 'require' calls ...
.js
后缀之类的tsc -noEmit
做类型检查时才会报,所以才可以用 @ts-ignore 忽略这个错误The text was updated successfully, but these errors were encountered: