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

Posted

技术标签:

【中文标题】在 Typescript 编译期间在相对导入语句上附加 .js 扩展名(ES6 模块)【英文标题】:Appending .js extension on relative import statements during Typescript compilation (ES6 modules) 【发布时间】:2020-10-18 11:46:44 【问题描述】:

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

这里是Hello World程序的目录结构和源代码:

目录结构:

| -- HelloWorldProgram
     | -- HelloWorld.ts
     | -- index.ts
     | -- package.json
     | -- tsconfig.json

index.ts:

import HelloWorld from "./HelloWorld";

let world = new HelloWorld();

HelloWorld.ts:

export class HelloWorld 
    constructor()
        console.log("Hello World!");
    

package.json:


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


现在,执行命令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'

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

希望上下文清楚。

现在,如何更改编译器输出设置(tsconfig.json 或任何标志),以便在 index.js 文件中的 Typescript 到 javascript 编译期间,本地相对路径导入(例如 import HelloWorld from ./Helloworld;)将被 import HelloWorld from ./Helloworld.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.

【问题讨论】:

只要import HelloWorld from "./HelloWorld.js"; TypeScript 足够聪明,可以在编译过程中找出你想要的是HelloWorld.ts TS 不会重写导入路径。 TS团队一直坚持这一点。编写在运行时有效的路径,并在需要时配置 ts 以使这些路径有效。 (在这种情况下不需要配置) @SonNguyen 作者已经知道这种解决方法,但它并没有帮助,因为这个问题源于一个旧项目,需要管理数百个文件和数百或数千个导入语句。如果您必须使用它,我们已经在使用比这更好的解决方法。 @TitianCernicova-Dragomir 是的,这对 TS 团队来说肯定是傲慢或自负,而且相当粗鲁。这是一项微不足道的任务,当然这个小功能对大多数开发人员都有帮助。至少在我们的例子中,如果编译器可以从生成的“.js”文件名本身中删除“.js”扩展名,这个问题会得到更好的解决。这就是我们目前正在做的——最后只是从所有 js 文件中删除扩展名,并留给服务器以正确的 MIME 类型提供服务。 @user3330840 这与傲慢无关,而是与设计原则有关。如果您在 typescript 中粘贴一段 Javascript,则两个版本之间应该没有运行时差异。当您开始重写路径时,您开始在两者之间产生差异。但我确实感到沮丧,这是很多人在关于这个主题的多个 github 问题上分享的。 【参考方案1】:

我通常只在 typescript 文件的 import 语句中使用 .js 扩展名,它也可以工作。

在导入路径中不使用文件扩展名是 nodejs 唯一的事情。由于您没有使用 commonjs 而是模块,因此您没有使用 nodejs。因此,您必须在导入路径中使用 .is 扩展名。

【讨论】:

【参考方案2】:

TypeScript 不可能知道你将使用什么 URI 来提供文件,因此它只是 必须相信你给它的模块路径是正确的。在这种情况下,你给了它一个不存在的 URI 的路径,但 TypeScript 不知道,所以它无能为力。

如果您使用以 .js 结尾的 URI 为模块提供服务,那么您的模块路径需要以 .js 结尾。如果您的模块路径不以.js 结尾,那么您需要在不以.js 结尾的URI 上提供它。

请注意the W3C strongly advises against using file extensions in URIs,因为它使您的系统更难发展,并主张转而依赖内容协商。

重写路径会破坏 TypeScript 的几个基本设计原则。一个设计原则是 TypeScript 是 ECMAScript 的适当超集,每个有效的 ECMAScript 程序和模块都是语义上等效的 TypeScript 程序和模块。重写路径会打破这个原则,因为一段 ECMAScript 的行为会根据它是作为 ECMAScript 还是 TypeScript 执行而有所不同。想象一下,你有以下代码:

./你好

export default "ECMAScript";

./hello.js

export default "TypeScript";

./main

import Hello from "./hello";

console.log(Hello);

如果 TypeScript 执行您的建议,这将打印 两种不同的东西,具体取决于您是作为 ECMAScript 还是作为 TypeScript 执行它,但是 TypeScript 设计原则说 TypeScript 永远不会改变ECMAScript 的含义。当我将一段 ECMAScript 作为 TypeScript 执行时,它的行为应该与我作为 ECMAScript 执行时的行为完全相同。

【讨论】:

这个例子牵强附会,用于保护它的设计原则完全脱离上下文,与 js 和 ts 文件之间必须存在一对一对应的实际问题无关. @user3330840:编译器无法选择它支持的语言规范的哪些部分。规范允许的任何内容由用户编写。事实上,在我当前的项目中,我非常地依赖于 TypeScript 不考虑模块路径这一事实,因为我允许最终用户提供多种不同语言(TypeScript、ECMAScript、CoffeeScript)的 API 兼容库, 和 JSON), 和模块加载器来选择正确的文件扩展名。 很遗憾这被否决了,因为它是正确的答案。 TypeScript 在这里的行为方式在某些常见情况下确实很烦人,但不幸的是,正确答案并不总是与我们希望正确的答案相同。 @DanielCassidy:正如我在上面的评论中提到的,“没人会写的牵强附会的例子”是我项目中的实际工作代码。该项目取决于,TypeScript 会准确地 加载我告诉它的模块,并且不会乱用路径。我希望所有来到这个问题的观众都会注意到,被接受的答案,这实际上是一个伪装成答案的咆哮,恰好是提问者写的。就个人而言,我真的不明白什么是那么难理解:如果你向 TypeScript 询问一个名为 foo 的模块,它会将其编译为代码…… ... 加载一个名为 foo 的模块。如果您向 TypeScript 询问名为 bar 的模块,它会将其编译为加载名为 bar 的模块的代码。如果您向 TypeScript 询问名为 bar.js 的模块,它会将其编译为加载名为 bar.js 的模块的代码。为什么它会做其他事情?【参考方案3】:

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

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

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

旁注:

关于 TS 团队在解决这个问题上的失败,似乎有一种趋势是试图断章取义地夸大这个问题,而不是把它附加到一些设计原则上来捍卫。

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

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

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

【讨论】:

【参考方案4】:

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

我认为 import 语句中的 having a flag to make typescript enforce file extensions 将是他们能做的最好的事情。然后像 eslint 这样的 linter 可能会根据该规则提供自动修复程序

【讨论】:

【参考方案5】:

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

用于导入json--experimental-json-modules 用于不带扩展名的导入--experimental-specifier-resolution=node

我知道--experimental-specifier-resolution=node 有一个错误(或没有)然后你不能运行没有扩展的 bin 脚本(例如在 package.json 中 bin "tsc" 不起作用,但 "tsc":"tsc.js" 会工作)。 很多包都有没有任何扩展的 bin 脚本,所以添加 NODE_OPTIONS="--experimental-specifier-resolution=node" 环境变量会有些麻烦

【讨论】:

谢谢 Andrew,这对我有用,我将它作为额外选项添加到我的 JS bin 入口点 #!/usr/bin/env node --experimental-specifier-resolution=node 的 shebang 行

以上是关于在 Typescript 编译期间在相对导入语句上附加 .js 扩展名(ES6 模块)的主要内容,如果未能解决你的问题,请参考以下文章

在编译期间估计相对 CPU 使用率

Typescript:esnext 编译器选项会破坏从外部库导入的 es6

TypeScript编译选型importsNotUsedAsValues含义

TypeScript编译选型importsNotUsedAsValues含义

TypeScript

Mocha + TypeScript:不能在模块外使用导入语句