繁体   English   中英

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

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

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

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

目录结构:

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

索引.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扩展名。

希望上下文清楚。

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

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

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

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

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

边注:

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

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

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

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

如果您在使用 TypeScript 和 ESM 时遇到问题,那么这个微型库实际上可以完美运行。

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

在脚本中用tsc-esm替换tsc调用:

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

最后你可以运行:

npm run build

为了修复此错误,您可以更改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
  }
}

这是NestJS使用的配置。

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

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

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

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

请注意, W3C强烈建议不要在 URIs 中使用文件扩展名,因为它会使您的系统更难发展,并主张转而依赖内容协商。

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

。/你好

export default "ECMAScript";

./hello.js

export default "TypeScript";

。/主要的

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 . 当我以 TypeScript 执行一段 ECMAScript 时,它的行为应该与我以 ECMAScript 执行时完全相同

正如许多人指出的那样。 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.

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

编辑添加了指向 eslint_plugin-import 的链接

您还可以添加 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"变量会有些麻烦

如果您知道所有导入语句确实应该具有.js扩展名,并且所有导入都没有扩展名或已经具有.js扩展名,那么您可以使用正则表达式查找/替换来“规范化”所有内容。 我建议您在提交更改之前检查您的 git(或其他 VCS)日志。 以下是我在 VSCode 中使用的正则表达式:

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

find 表达式将匹配没有.js扩展名的导入。 第一组将捕获引号内的部分。 然后,替换表达式采用第一组匹配项,该组始终没有.js扩展名,然后附加扩展名。

如果设置 linter 失败,您可以定期运行它并检查 git 日志,以确保在没有扩展名的情况下没有导入代码库。

在大型 monorepo 上遇到了同样的问题,无法手动编辑每个文件,所以我编写了一个脚本来修复我的项目中的所有 esm 导入和 append .js/index.js以安全的方式:

修复 esm 导入路径

在您的项目中使用之前进行测试。

如果您使用的是 VS 代码,则可以使用正则表达式替换路径。

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

替换: $1.js$2

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

忽略 npm 导入。 例子:

import fs from 'fs'

支持多行导入。 例子:

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

支持as进口。 例子:

import * as foo from './file'

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

import foo from "./file"

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

export { foo } from './file'

您可以使用与我相同的解决方案

文件:tsconfig.json

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

因为使用commonjs,所以必须去掉package.json中的“type”:“module”

完成:D

暂无
暂无

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

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