在 Typescript 编译(ES6 模块)期间在相对导入语句上附加 .js 扩展名

use*_*840 16 javascript node.js npm node-modules typescript

这似乎是一个微不足道的问题,但需要使用什么设置/配置来解决这个问题并不是很明显。

下面是Hello World程序目录结构和源代码:

目录结构:

| -- HelloWorldProgram
     | -- HelloWorld.ts
     | -- index.ts
     | -- package.json
     | -- tsconfig.json
Run Code Online (Sandbox Code Playgroud)

索引.ts:

import {HelloWorld} from "./HelloWorld";

let world = new HelloWorld();

Run Code Online (Sandbox Code Playgroud)

HelloWorld.ts:

export class HelloWorld {
    constructor(){
        console.log("Hello World!");
    }
}
Run Code Online (Sandbox Code Playgroud)

包.json:

{
  "type": "module",
  "scripts": {
    "start": "tsc && node index.js"
  }
}

Run Code Online (Sandbox Code Playgroud)

现在,执行该命令会 tsc && node index.js导致以下错误:

internal/modules/run_main.js:54
    internalBinding('errors').triggerUncaughtException(
                              ^

Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'HelloWorld' imported from HelloWorld\index.js
Did you mean to import ../HelloWorld.js?
    at finalizeResolution (internal/modules/esm/resolve.js:284:11)
    at moduleResolve (internal/modules/esm/resolve.js:662:10)
    at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:752:11)
    at Loader.resolve (internal/modules/esm/loader.js:97:40)
    at Loader.getModuleJob (internal/modules/esm/loader.js:242:28)
    at ModuleWrap.<anonymous> (internal/modules/esm/module_job.js:50:40)
    at link (internal/modules/esm/module_job.js:49:36) {
  code: 'ERR_MODULE_NOT_FOUND'
}
Run Code Online (Sandbox Code Playgroud)

很明显,这个问题似乎源于这样一个事实,即在index.tsTypescript 文件.js中,import 语句 ( import {HelloWorld} from "./HelloWorld";) 中没有扩展名。Typescript 在编译期间没有抛出任何错误。但是,在运行时 Node (v14.4.0) 想要.js扩展。

希望上下文清楚。

现在,如何更改编译器输出设置(tsconfig.json 或任何标志),以便在 Typescript 到 Javascript 编译期间import {HelloWorld} from ./Helloworld;将本地相对路径导入替换import {HelloWorld} from ./Helloworld.js;index.js文件中的?

笔记: It is possible to directly use the .js extension while importing inside typescript file. However, it doesn't help much while working with hundreds of old typescript modules, because then we have to go back and manually add .js extension. Rather than that for us better solution is to batch rename and remove all the .js extension from all the generated .js filenames at last.

enr*_*r99 37

如果您使用 VS code,则可以使用正则表达式来替换路径。

寻找:(\bfrom\s+["']\..*)(["'])

代替:$1.js$2

该解决方案的灵感来自于之前的解决方案,但由于下述原因,该解决方案效果更好。请注意,此解决方案并不完美,因为它使用正则表达式而不是对文件导入进行语法分析。

忽略 npm 导入。例子:

import fs from 'fs'
Run Code Online (Sandbox Code Playgroud)

支持多行导入。例子:

import {
  foo,
  bar
} from './file'
Run Code Online (Sandbox Code Playgroud)

支持as进口。例子:

import * as foo from './file'
Run Code Online (Sandbox Code Playgroud)

支持单引号和双引号。例子:

import foo from './file'
import foo from "./file"
Run Code Online (Sandbox Code Playgroud)

支持出口。请参阅导出文档。例子:

export { foo } from './file'
Run Code Online (Sandbox Code Playgroud)

  • `(from\s+)(["'])(?!.*\.js)(\.?\.\/.*)(["'])` 与 .js 不匹配,并为您提供捕获在 VS Code 中使用 `$1$2$3.js$4` 写回的组 (6认同)

小智 27

您还可以添加 nodejs CLI 标志以启用node module resolution

  • 用于导入 json--experimental-json-modules
  • 用于不带扩展名的导入--experimental-specifier-resolution=node
node --experimental-specifier-resolution=node dist/some-file.js
Run Code Online (Sandbox Code Playgroud)

值得一提的是,--experimental-specifier-resolution=node如果有一个错误(或没有),那么您就无法运行bin没有扩展名的脚本(例如在 package.jsonbin部分中,"tsc"不会工作,但"tsc":"tsc.js"会工作)。

太多包的bin脚本没有任何扩展名,因此添加NODE_OPTIONS="--experimental-specifier-resolution=node"环境变量会遇到一些问题

  • 这个节点标志是最好的解决方案 - 它确保您不必向所有导入添加“.js”扩展名,它可以与nodemon和supervisor一起使用,并且可以与jest一起使用(我遇到了添加.js的情况)运行服务器时扩展,但 .js 扩展在运行 jest 时破坏了我的测试 - 此节点标志解决了这个问题)。最初在这里看到了同样的解决方案 - /sf/answers/4947795821/。 (3认同)
  • 不幸的是,这个解决方案不适用于节点 19。有人有替代方案吗? (3认同)
  • 谢谢安德鲁,这对我有用,我将它作为额外选项添加到我的 JS bin 入口点 `#!/usr/bin/env node --experimental-specifier-resolution=node` 的 shebang 行上 (2认同)

use*_*840 18

对于正在寻找此问题解决方案的开发人员,我们遇到的可能解决方法如下:

  1. 对于新文件,可以".js"在编辑时简单地在 Typescript 文件的 import 语句中添加扩展名。例子:import {HelloWorld} from "./HelloWorld.js";

  2. 如果使用旧项目,而不是遍历每个文件并更新导入语句,我们发现".js"通过简单的自动化脚本简单地批量重命名并从生成的 Javascript 中删除扩展名更容易。但是请注意,这可能需要对服务器端代码稍作更改,以将这些".js"具有正确 MIME 类型的无扩展名文件提供给客户端。如果您想避免这种情况,您可能需要使用正则表达式来批量查找和递归替换导入语句以添加.js扩展。

边注:

对于 TS 团队在解决这个问题上的失败,似乎有一种倾向,试图将这个问题断章取义,而不是将其附加到一些设计原则上进行辩护。

然而,实际上这只不过是编译器如何不对称地处理扩展名的问题。Typescript 编译器允许没有扩展名的 import 语句。然后在文件被翻译时继续将“.js”扩展名添加到相应的输出文件名,但对于引用此文件的相应导入语句,它忽略了它在翻译期间添加了“.js”扩展名的事实。脱离上下文的 URI 重写原则如何保护这种不对称性?

Typescript 文件和编译时生成的 Javascript 输出文件有固定的一一对应关系。如果引用的导入不存在,编译器会抛出错误。这些文件甚至不会编译!因此,在上下文或不可编译的示例中提到其他冲突 URI 的可能性会使此类声明无效。

如果编译器只是生成无扩展名的输出文件,它也可以解决这个问题。但是,这是否也会以某种方式违反有关 URI 重写的设计原则?当然,在这种情况下,可能存在其他设计原则来捍卫这一立场!但这样的顽固岂不是进一步验证了TS团队在这个问题上的顽固或无知?

  • 然而,实际上这只不过是**编译器如何不对称地处理扩展**的问题。Typescript 编译器允许不带扩展名的 import 语句。然后,在翻译文件时,它会继续将“.js”扩展名添加到相应的输出文件名中,但对于引用该文件的相应导入语句,它会忽略在翻译过程中添加“.js”扩展名的事实。如何通过脱离上下文的 URI 重写原则来保护这种不对称性? (3认同)
  • Typescript 文件和编译过程中生成的 Javascript 输出文件之间存在固定的一一对应关系。如果引用的导入不存在,编译器将抛出错误。这些文件甚至无法编译!因此,脱离上下文或不可编译的示例提及其他冲突 URI 的可能性会使此类声明无效。 (3认同)
  • 此评论和其余评论均为原始旁注,移动为评论:关于 TS 团队在解决此问题上的失败,似乎存在试图断章取义地夸大此问题而不是其实际情况的倾向,并附加这要捍卫一些设计原则。 (2认同)
  • 如果编译器只是生成无扩展名的输出文件,它也可以解决该问题。但是,这是否也会违反有关 URI 重写的设计原则?当然,在这种情况下,可能存在其他设计原则来捍卫这一立场!但这样的固执岂不是更加证实了TS团队在这个问题上的强硬或者无知吗? (2认同)
  • 最后,没有必要为这个问题辩护,因为这个问题是真实的,而且现在已经很明显了。同样,无论是语言、设计选择还是设计原则等,外部可选的简单后编译工具都可以简单地解决这个问题。因此,对于那些捍卫设计选择的评论或答案,虽然您的观点可能是有效的,但它们并没有解决开发人员面临的真正问题,并且该问题无意争论设计选择。唯一的目标是找到可能的解决方案并帮助新手程序员。 (2认同)

Pat*_*ick 11

另一种解决方案是使用tsc-alias它有一个resolveFullPaths选项。

尝试用完全解析的路径替换不完整的导入路径(不以 .js 结尾的路径)(以实现 ECMAScript 模块兼容性)

我最初安装和设置是tsc-alias为了解决一些别名导入(我喜欢避免使用父导入,但在必要的情况下,例如常量或实用函数,我使用别名)。一旦工作,我发现该resolveFullPaths选项解决了OP描述的问题。

  1. 安装tsc-alias

    yarn add -D -E tsc-alias
    
    Run Code Online (Sandbox Code Playgroud)
  2. 配置tsconfig.js

    {
      "compilerOptions": {
        ...
      },
      "tsc-alias": {
        "resolveFullPaths": true,
        "verbose": false
      }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  3. 更新您的构建/编译脚本以tsc-alias在之后调用tsc

    "scripts": {
        "compile": "tsc && tsc-alias"
    }
    
    Run Code Online (Sandbox Code Playgroud)


pan*_*ter 9

npm,有人吗?

npm i fix-esm-import-path
Run Code Online (Sandbox Code Playgroud)

在npmjsgithub上检查。

只有 8 颗星(一颗星来自我),但我在多个项目中使用它,它满足了我的需要:

npx fix-esm-import-path dist/your-compiled-entrypoint.js
Run Code Online (Sandbox Code Playgroud)


小智 7

正如许多人指出的那样。TypeScript 不会也永远不会向 import 语句添加文件扩展名的原因是它们的前提是转译纯 JavaScript 代码应该输出相同的 JavaScript 代码。

我认为有一个标志让 TypeScript在导入语句中强制执行文件扩展名将是他们能做的最好的事情。然后,像ESLint这样的 linter可能会根据该规则提供自动修复程序。


red*_*dia 5

如果您在使用 TypeScript 和 ESM 时遇到问题,这个小库实际上可以完美运行:

npm install @digitak/tsc-esm --save-dev
Run Code Online (Sandbox Code Playgroud)

将脚本中的tsc调用替换为:tsc-esm

{
   "scripts": {
      "build": "tsc-esm"
   }
}
Run Code Online (Sandbox Code Playgroud)

最后你可以运行:

npm run build
Run Code Online (Sandbox Code Playgroud)

  • 作者:我不建议再使用这个。从 TS 4.7 开始,现在可以将 moduleResolution 字段设置为 node16 或 nodeNext,这强制在导入末尾使用 .js 扩展名。我首先开发了 tsc-esm 作为 tsc 编译器的补丁,它将添加 .js 扩展名。但这种技术有缺点。例如,它无法处理 Typescript 别名。我强烈鼓励遵循 Ecmascript 指南和最近的 Typescript 扩展,即使用 Typescript@^4.7 并将 moduleResolution 字段设置为 tsconfig.json 中的 node16 或 nodeNext。 (8认同)