NodeJS 如何防止typescript将动态导入转换为require()?

bkkx9g8r  于 11个月前  发布在  Node.js
关注(0)|答案(8)|浏览(159)

我正在构建一个discord.js Discord机器人。由于某种原因,discord.js不能与ESM模块一起工作(一个完全独立的问题),所以我的机器人应用程序使用CommonJS模块。现在我的系统上有另一个名为Lib的项目,它有很多实用函数,我计划在几个不同的项目中使用,所以我不必重写它们。这个Lib项目使用ESM模块。由于我我必须从DiscordBot导入Lib,我使用typescript中的动态导入语法。现在,每当我转译我的DiscordBot项目时,动态导入都会转换为 * 一些丑陋的JavaScript模块代码 *,而这些丑陋的模块代码最终使用require()。由于require()无法导入ESM模块,我的bot最终崩溃。
然而,我试图停止我的ts编译器,从导入Lib的ts文件中复制代码,然后手动将该代码粘贴到相应的JS文件中(并删除TS专有的功能,如类型注解和接口)。然后我运行我的bot应用程序,但是我不想每次都这样做,所以问题出在tsc的编译上,我该怎么解决呢?

hgb9j2n6

hgb9j2n61#

所以我理解的目的是:
1.使用TypeScript开发代码
1.运行CommonJS包中的编译代码
1.导入并使用ES模块

选项一:

如果tsconfig.json中的"module"被设置为"commonjs",目前没有办法阻止TypeScript将动态import()转换为require()-除了将代码隐藏在字符串中并使用eval执行它。像这样:

async function body (pMap:any){
   // do something with module pMap here
}

eval ("import('p-map').then(body)");

字符串
TypeScript不可能转译字符串!

选项二

tsconfig.json中的"module"设置为"es2020"。这样做,动态导入就不会被转译成require(),你可以使用动态导入来导入CommonJS或ES模块。或者,可以使用const someModule = require("someModule")语法导入CommonJS模块(不会转换为ES6导入语法)。您不能使用ES6导入语法,如import * as someModule from "someModule"import someModule from "someModule"。这些语法将发出ES Module语法导入(“模块”设置为“es 2020”),无法在CommonJS包中运行。
下面是一点信息:

  • 如果"module"设置为"es2020":动态导入import()不会被转译。
  • 如果"module"被设置为“es 2015”:则会出现错误:

TS 1323:仅当“--module”标志设置为“es 2020”、“esnext”、“commonjs”、“amd”、“system”或“umd”时,才支持动态导入。

  • 如果"module"被设置为"commonjs":动态导入被转译。

引用"module"字段的tsconfig.json参考:
如果你想知道ES 2015和ES 2020之间的区别,ES 2020增加了对动态导入和import. meta的支持。

svmlkihl

svmlkihl2#

其他人谈论的node12设置对我不起作用,但这些compilerOptions可以,使用Typescript 4.7.2:

"module": "CommonJS",
"moduleResolution": "Node16",

字符串
这节省了我的背部,我不必将所有import require迁移到导入,以便能够使用ESM npm库。
Typescript输入源:

import Redis = require('redis');
import * as _ from 'lodash';

export async function main() {
    const fileType = await import('file-type');
    console.log(fileType, _.get, Redis);
}


CommonJS输出:

...
const Redis = require("redis");
const _ = __importStar(require("lodash"));
async function main() {
    const fileType = await import('file-type');
    console.log(fileType, _.get, Redis);
}
exports.main = main;

8nuwlpux

8nuwlpux3#

这目前是不可能的。GitHub(https://github.com/microsoft/TypeScript/issues/43329)有一个非常新的问题,但尚未实现。所以你现在所能做的就是用你的Lib项目从ESM切换到CommonJS。

更新2022

这个问题已经解决,现在有一个新的"module"选项,名为node12。这应该可以解决这个问题。

6ojccjat

6ojccjat4#

我正在使用已经提到的基于eval的黑客的一个变体来克服这个问题。
例如,parse-domain是作为ESM模块分发的,因此像这样导入它会破坏基于CJS的节点应用程序:

import { fromUrl, parseDomain } from 'parse-domain';

const parseDomainFromUrl = (url: string) => {
    return parseDomain(fromUrl(url));
}

字符串
这就是我如何让它工作的:

const dynamicImport = new Function('specifier', 'return import(specifier)');

const parseDomainFromUrl = (url: string) => {
  return dynamicImport('parse-domain').then((module: any) => {
    const { fromUrl, parseDomain } = module;
    return parseDomain(fromUrl(url));
  })
};


(Note parseDomainFromUrl在进程中变得异步,因此调用者需要等待它。

8wigbo56

8wigbo565#

添加我的答案,因为这显然仍然是2023年的一个问题,没有解决方案,保存eval hack是万无一失的。使用commonjs以外的任何东西作为模块设置可能会破坏其他模块的编译。在我们的例子中,firebase模块的负载在设置commonjs以外的任何东西时都有问题。
具体来说,我们需要阻止typescript将动态导入转换为require
对我们有效的选项是将module选项设置为"NodeNext",然后将skipLibCheck设置为true,以抑制由我们导入的依赖项之一引起的错误。这不像我们真的可以用该依赖项更改任何内容(因为它被googlecloud模块使用)。
根据你的观点,这并不理想,但js模块系统也不理想。下面粘贴我们的tsconfig.json配置:

{
  "compilerOptions": {
    "module": "NodeNext",
    "skipLibCheck": true,
    "allowSyntheticDefaultImports": true,
    "noImplicitReturns": true,
    "noUnusedLocals": false,
    "outDir": "lib",
    "sourceMap": true,
    "strict": true,
    "target": "ES2020",
    "esModuleInterop": true
  },
  "compileOnSave": true,
  "include": ["src"],
  "exclude": ["debug_scripts"]
}

字符串

qnzebej0

qnzebej06#

这个问题已通过为module设置添加node12选项得到修复。从文档中:
node 12和nodenext模式在夜间版本中可用,实验性的node 12和nodenext模式与Node的原生ECMAScript Module支持集成。发出的JavaScript使用CommonJS或ES 2020输出,具体取决于文件扩展名和最近的package.json中的类型设置值。模块解析也有不同的工作方式。您可以在手册中了解更多信息。
但是,如果您使用此设置而不进行夜间构建,则当前会产生以下错误:
错误TS 4124:值为“node 12”的编译器选项“module”不稳定。请使用夜间TypeScript消除此错误。请尝试使用“npm install -D typescript@ next”进行更新。

kcwpcxri

kcwpcxri7#

我参加聚会要迟到了,但我也想在这里留下我的解决方案。使用js eval(不需要包,这是一个原生的js函数)我创建了一个approxc类型的esm模块导入器:

export async function importEsmModule<T>(
  name: string
): Promise<T> {
  const module = eval(
    `(async () => {return await import("${ name }")})()`
  )
  return module as T
}

//
// usage
//

import type MyModuleType from 'myesmmodule'

const MyModule = await importEsmModule<typeof MyModuleType>('myesmmodule')

字符串
对我来说,这就像一个魅力:)
如果你愿意,我还创建了this npm包。
希望这对某人有帮助!

q7solyqu

q7solyqu8#

你用的是什么编译器/打包器?我假设tsc是基于上下文的。
我推荐使用esbuild来编译和捆绑你的TS。你也可以在使用tsc之后使用它来简单地转换它。它有一个名为“format”的选项,可以删除任何模块样式的导入。请访问https://esbuild.github.io/api/#format。
以下是使用的简单范例。
build.js

const esbuild = require("esbuild");

esbuild.build({
   allowOverwrite: true,
   write: true,
   entryPoints: ["my-main-file.ts"],
   outfile: "some-file.bundle.js",
   format: "cjs", //format option set to cjs makes all imports common-js style
   bundle: true,
}).then(() => {
   console.log("Done!");
});

字符串
然后,您可以向package.json中添加如下内容

"scripts": {
        "build": "node build.js",
...rest of scripts


这里有一个附加的链接,是关于使用带有typescript的esbuild的一些注意事项。这些对您来说都不是问题。https://esbuild.github.io/content-types/#typescript-caveats

相关问题