简体   繁体   English

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

[英]Appending .js extension on relative import statements during Typescript compilation (ES6 modules)

This seems to be a trivial problem, but it is not very obvious what settings/configurations need to be used to solve this issue.这似乎是一个微不足道的问题,但解决此问题需要使用哪些设置/配置并不是很明显。

Here are the Hello World program directory structure and the source code:下面是 Hello World 程序目录结构和源代码:

Directory Structure:目录结构:

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

index.ts:索引.ts:

import {HelloWorld} from "./HelloWorld";

let world = new HelloWorld();

HelloWorld.ts: HelloWorld.ts:

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

package.json: package.json:

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

Now, execution of the command tsc && node index.js results in the following error:现在,执行命令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'
}

It is obvious that the problem seems to have been originated from the fact that in index.ts Typescript file there is no .js extension in the import statement ( import {HelloWorld} from "./HelloWorld"; ).很明显,问题似乎源于index.ts Typescript 文件中 import 语句中没有.js扩展名( import {HelloWorld} from "./HelloWorld"; )。 Typescript didn't throw any error during compilation. Typescript 在编译过程中没有抛出任何错误。 However, during runtime Node (v14.4.0) wants the .js extension.但是,在运行时 Node (v14.4.0) 需要.js扩展名。

Hope the context is clear.希望上下文清楚。

Now, how to change the compiler output setting (tsconfig.json or any flags) so that local relative path imports such as import {HelloWorld} from./Helloworld;现在,如何更改编译器 output 设置(tsconfig.json 或任何标志)以便本地相对路径导入,例如import {HelloWorld} from./Helloworld; will get replaced by import {HelloWorld} from./Helloworld.js;将被替换为import {HelloWorld} from./Helloworld.js; during Typescript to Javascript compilation in the index.js file?index.js文件中 Typescript 到 Javascript 编译期间?

Note: 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.注意: 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. 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.

To fellow developers who are looking for a solution to this issue, the possible work-arounds we have come across are as follows:对于正在寻找此问题的解决方案的开发人员,我们遇到的可能解决方法如下:

  1. For new files, it is possible to simply add ".js" extension in the import statement in Typescript file while editing.对于新文件,可以在编辑时在 Typescript 文件的导入语句中简单地添加".js"扩展名。 Example: import {HelloWorld} from "./HelloWorld.js";示例: import {HelloWorld} from "./HelloWorld.js";

  2. If working with old projects, rather than going through each and every file and updating the import statements, we found it easier to simply batch rename and remove the ".js" extension from the generated Javascript via a simple automated script.如果使用旧项目,而不是遍历每个文件并更新导入语句,我们发现通过简单的自动化脚本从生成的 Javascript 中简单地批量重命名和删除".js"扩展名更容易。 Please note however that this might require a minor change in the server side code to serve these extension-less ".js" files with the proper MIME type to the clients.但是请注意,这可能需要对服务器端代码进行细微更改,以便为客户端提供这些具有正确 MIME 类型的无扩展名".js"文件。 If you want to avoid this, you may instead want to use regular expression to batch find and replace the import statements recursively to add the .js extension.如果您想避免这种情况,您可能希望使用正则表达式以递归方式批量查找和替换导入语句以添加.js扩展名。

Side note:边注:

Regarding the TS team's failure on resolving this issue, it appears that there is a tendency to try to blow up this issue out of context than what it really is and attach that to some design principles to defend.关于 TS 团队在解决这个问题上的失败,似乎有一种趋势是试图断章取义地炸毁这个问题,而不是把它附加到一些设计原则上来捍卫。

However, factually this is nothing more than an issue with how the compiler deals asymmetrically with the extension .然而,实际上这只不过是编译器如何不对称地处理扩展名的问题。 The Typescript compiler allows import statement without an extension. Typescript 编译器允许不带扩展名的导入语句。 It then goes on to add ".js" extension to the corresponding output filename while the file is being translated, but for the corresponding import statements where this file is referenced it ignores the fact that it has added ".js" extension during translation.然后,它继续在文件被翻译时将“.js”扩展名添加到相应的 output 文件名,但对于引用该文件的相应导入语句,它忽略了它在翻译期间添加了“.js”扩展名的事实。 How can this asymmetricity be defended by the out of context URI rewriting principles?脱离上下文的 URI 重写原则如何保护这种不对称性?

There is a fixed one to one correspondence between the Typescript file and the generated Javascript output file during compilation. Typescript文件与编译时生成的Javascript output文件之间存在固定的一一对应关系。 If the referenced import does not exists, the compiler would throw an error.如果引用的导入不存在,编译器会抛出错误。 The files wouldn't even compile, So.这些文件甚至不会编译,所以。 out of context or non-compilable examples mentioning the possibility of other conflicting URIs invalidate such claims.提及其他冲突 URI 的可能性的断章取义或不可编译的示例使此类声明无效。

If the compiler simply generated extension-less output files it would also solve the issue .如果编译器只生成无扩展名的 output 文件,它也可以解决问题 But, would that also somehow violate the design principle regarding URI rewrites?但是,这也会以某种方式违反有关 URI 重写的设计原则吗? Certainly, in that case there could exist other design principles to defend the position?当然,在那种情况下,可能存在其他设计原则来保护 position? But wouldn't such stubbornness only help to further validate the adamancy or ignorance of the TS team on this issue?但这样的固执,岂不是更加印证了TS团队在这个问题上的执着或无知?

In case you have trouble with TypeScript and ESM, there is this tiny library that actual works perfect.如果您在使用 TypeScript 和 ESM 时遇到问题,那么这个微型库实际上可以完美运行。

npm install @digitak/tsc-esm --save-dev

Replace the tsc call with tsc-esm in your scripts:在脚本中用tsc-esm替换tsc调用:

{
   "scripts": {
      "build": "tsc-esm"
   }
}

Finally you can run:最后你可以运行:

npm run build

In order to fix this error, you can change your tsconfig.json file as following:为了修复此错误,您可以更改tsconfig.json文件,如下所示:

{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "es2017",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true,
    "resolveJsonModule": true
  }
}

This is the configuration that NestJS uses.这是NestJS使用的配置。

I usually just use the.js extension in import statements in typescript files as well and it works.我通常只在 typescript 文件的导入语句中使用 .js 扩展名,它也可以工作。

Not using a file extension in import paths is a nodejs only thing.在导入路径中不使用文件扩展名是 nodejs 唯一的事情。 Since you are not using commonjs but module you are not using nodejs.由于您没有使用 commonjs 而是模块,因此您没有使用 nodejs。 Therefore you have to use the.is extension in import paths.因此,您必须在导入路径中使用 .is 扩展名。

TypeScript cannot possibly know what URI you are going to use to serve your files, therefore it simply must trust that the module path you gave it is correct. TypeScript不可能知道您将使用什么 URI 来提供文件,因此它必须相信您提供的模块路径是正确的。 In this case, you gave it a path to a URI that doesn't exist, but TypeScript cannot know that, so there is nothing it can do about it.在这种情况下,你给它一个不存在的 URI 的路径,但 TypeScript 不知道,所以它无能为力。

If you are serving the module with a URI that ends in .js , then your module path needs to end in .js .如果您使用以.js结尾的 URI 为模块提供服务,那么您的模块路径需要以.js结尾。 If your module path doesn't end in .js , then you need to serve it up at a URI that does not end in .js .如果您的模块路径不以.js结尾,那么您需要在不以.js结尾的 URI 上提供它。

Note that the W3C strongly advises against using file extensions in URIs , because it makes it harder to evolve your system, and advocates to instead rely on Content Negotiation.请注意, W3C强烈建议不要在 URIs 中使用文件扩展名,因为它会使您的系统更难发展,并主张转而依赖内容协商。

Rewriting paths would break a couple of fundamental design principles of TypeScript.重写路径会破坏 TypeScript 的几个基本设计原则。 One design principle is that TypeScript is a proper superset of ECMAScript and every valid ECMAScript program and module is a semantically equivalent TypeScript program and module.一个设计原则是 TypeScript 是 ECMAScript 的适当超集,每个有效的 ECMAScript 程序和模块都是语义上等效的 TypeScript 程序和模块。 Rewriting paths would break that principle, because a piece of ECMAScript would behave differently depending on whether it is executed as ECMAScript or TypeScript.重写路径会破坏这个原则,因为一段 ECMAScript 的行为会根据它是作为 ECMAScript 还是 TypeScript 执行而有所不同。 Imagine, you have the following code:想象一下,您有以下代码:

./hello 。/你好

export default "ECMAScript";

./hello.js ./hello.js

export default "TypeScript";

./main 。/主要的

import Hello from "./hello";

console.log(Hello);

If TypeScript did what you suggest, this would print two different things depending on whether you execute it as ECMAScript or as TypeScript, but the TypeScript design principles say that TypeScript does never change the meaning of ECMAScript . If TypeScript did what you suggest, this would print two different things depending on whether you execute it as ECMAScript or as TypeScript, but the TypeScript design principles say that TypeScript does never change the meaning of ECMAScript . When I execute a piece of ECMAScript as TypeScript, it should behave exactly as it does when I execute it as ECMAScript.当我以 TypeScript 执行一段 ECMAScript 时,它的行为应该与我以 ECMAScript 执行时完全相同

As many have pointed out.正如许多人指出的那样。 The reason why Typescript doesn'y and will never add file extension to import statements is their premise that transpiling pure javascript code should output the same javascript code. The reason why Typescript doesn'y and will never add file extension to import statements is their premise that transpiling pure javascript code should output the same javascript code.

I think having a flag to make typescript enforce file extensions in import statements would be the best they could do.我认为有一个标志让 typescript 在导入语句中强制执行文件扩展名将是他们能做的最好的事情。 Then linters like eslint could maybe offer an auto fixer based on that rule然后像 eslint 这样的linter可能会根据该规则提供自动修复程序

edit added a link to eslint_plugin-import编辑添加了指向 eslint_plugin-import 的链接

you also can add nodejs CLI flags for enable node module resolution :您还可以添加 nodejs CLI 标志以启用node module resolution

  • for importing json --experimental-json-modules用于导入 json --experimental-json-modules
  • for importing without extensions --experimental-specifier-resolution=node用于不带扩展名的导入--experimental-specifier-resolution=node

I know --experimental-specifier-resolution=node has a bug (or not) then you cannot run bin scripts without extensions (for example in package.json bin "tsc" wan't work, but "tsc":"tsc.js" will work).我知道--experimental-specifier-resolution=node有一个错误(或没有),那么你不能运行没有扩展的 bin 脚本(例如在 package.json bin "tsc" 不起作用,但 "tsc":"tsc. js”将起作用)。 To many packages has bin scripts without any extensions so there is some trouble with adding NODE_OPTIONS="--experimental-specifier-resolution=node" env variable对于许多软件包来说,bin 脚本没有任何扩展,因此添加NODE_OPTIONS="--experimental-specifier-resolution=node"变量会有些麻烦

If you know that all your import statements should really have the .js extension, and all imports either have no extension or already have the .js extension, you could use a regex find/replace to "normalise" everything.如果您知道所有导入语句确实应该具有.js扩展名,并且所有导入都没有扩展名或已经具有.js扩展名,那么您可以使用正则表达式查找/替换来“规范化”所有内容。 I would advise you just check your git (or other VCS) logs before committing the change.我建议您在提交更改之前检查您的 git(或其他 VCS)日志。 Here are the regexes I use in VSCode:以下是我在 VSCode 中使用的正则表达式:

  • Find: (import.* from ".*(?.\.js)(.){3})" .查找: (import.* from ".*(?.\.js)(.){3})"
  • Replace: $1.js .替换: $1.js

The find expression will match imports without the .js extension. find 表达式将匹配没有.js扩展名的导入。 The first group will capture the part inside the quotes.第一组将捕获引号内的部分。 The replace expression then takes the first group of the match, which always doesn't have the .js extension, and then appends the extension.然后,替换表达式采用第一组匹配项,该组始终没有.js扩展名,然后附加扩展名。

Failing getting a linter set up, you could run this periodically & check the git logs to ensure no imports without the extension slip into the codebase.如果设置 linter 失败,您可以定期运行它并检查 git 日志,以确保在没有扩展名的情况下没有导入代码库。

Had the same issue on a big monorepo, can't edit each file manually, so I wrote a script to fix all esm import in my project and append .js or /index.js in a safe way:在大型 monorepo 上遇到了同样的问题,无法手动编辑每个文件,所以我编写了一个脚本来修复我的项目中的所有 esm 导入和 append .js/index.js以安全的方式:

fix-esm-import-paths 修复 esm 导入路径

Test before using in your project.在您的项目中使用之前进行测试。

If you are using VS code, you can use regex to replace the paths.如果您使用的是 VS 代码,则可以使用正则表达式替换路径。

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

Replace: $1.js$2替换: $1.js$2

This solution is inspired on a previous solution, but this one works better because of the reasons outlined below.此解决方案的灵感来自先前的解决方案,但由于以下原因,此解决方案效果更好。 Note that this solution is not perfect since it uses regex instead of syntactically analyzing file imports.请注意,此解决方案并不完美,因为它使用正则表达式而不是语法分析文件导入。

Ignores npm imports.忽略 npm 导入。 Example:例子:

import fs from 'fs'

Supports multi-line imports.支持多行导入。 Example:例子:

import {
  foo,
  bar
} from './file'

Supports as imports.支持as进口。 Example:例子:

import * as foo from './file'

Supports single and double quotes.支持单引号和双引号。 Example:例子:

import foo from "./file"

Supports exports.支持导出。 See export docs.请参阅导出文档。 Example:例子:

export { foo } from './file'

You can use the same solution as me您可以使用与我相同的解决方案

File: tsconfig.json文件:tsconfig.json

"compilerOptions": {
    "module": "commonjs",   ==> not required extension when import   
    "target": "ES6",
},

Because use commonjs, you must remove "type": "module" in package.json因为使用commonjs,所以必须去掉package.json中的“type”:“module”

Done:D完成:D

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM