在类型脚本编译期间(ES6模块) ,在相关导入语句上附加.js 扩展名

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

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

目录结构:

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

索引:

import {HelloWorld} from "./HelloWorld";


let world = new HelloWorld();


HelloWorldd.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类型脚本文件中,import 语句(import {HelloWorld} from "./HelloWorld";)中没有 .js扩展名。在编译过程中,类型脚本没有抛出任何错误。但是,在运行时 Node (v14.4.0)需要 .js扩展。

希望上下文说清楚了。

现在,如何更改编译器输出设置(tsconfig.json 或任何标志) ,以便在 index.js文件中的 Typecript to 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.

19599 次浏览

I usually just use the .js extension in import statements in typescript files as well and it works.

Not using a file extension in import paths is a nodejs only thing. Since you are not using commonjs but module you are not using nodejs. Therefore you have to use the .is extension in import paths.

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. 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.

If you are serving the module with a URI that ends in .js, then your module path needs to end in .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.

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.

Rewriting paths would break a couple of fundamental design principles of 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. Rewriting paths would break that principle, because a piece of ECMAScript would behave differently depending on whether it is executed as ECMAScript or TypeScript. Imagine, you have the following code:

./hello

export default "ECMAScript";

./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. When I execute a piece of ECMAScript as TypeScript, it should behave exactly as it does when I execute it as ECMAScript.

To fellow developers who are looking for a solution to this issue, the possible work-arounds we have come across are as follows:

  1. Use .js extension in the import:

For new files, it is possible to simply add ".js" extension in the import statement in Typescript file while editing. Example: import {HelloWorld} from "./HelloWorld.js";

  1. Extensionless filename

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. 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.

  1. Use regex to batch replace import statements

Another option is to use regular expression to batch find and replace in all files the import statements to add the .js extension. An example: https://stackoverflow.com/a/73075563/3330840 or similar other answers.

Updated side note:

Initially, some answers and comments here created unnecessary distractions and tried to evade the original purpose of the question instead of providing possible solutions and dragged me into having to defend the validity of the problem. 16k+ views on this question indicates many developers were faced with this issue as well which itself proves the importance of the question. Hence, the original side note now has been moved to the comments to avoid further distraction.

As many have pointed out. The reason why Typescript doesn't 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. Then linters like eslint could maybe offer an auto fixer based on that rule

you also can add nodejs CLI flags for enable node module resolution:

  • for importing json --experimental-json-modules
  • for importing without extensions --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). To many packages has bin scripts without any extensions so there is some trouble with adding NODE_OPTIONS="--experimental-specifier-resolution=node" env variable

You can use the same solution as me

File: tsconfig.json

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

Because use commonjs, you must remove "type": "module" in package.json

Done :D

In case you have trouble with TypeScript and ESM, there is this tiny library that actual works perfect.

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

Replace the tsc call with tsc-esm in your scripts:

{
"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:

{
"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.

(Probably) Better Answer

See here for a potentially better answer based on this idea & proposed in the comments.

I haven't tested this yet, but seems like my original answer below is lacking & seems like the linked answer is better. I can't say for sure but I'd recommend people check that out first.

My Original Answer

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. I would advise you just check your git (or other VCS) logs before committing the change. Here are the regexes I use in VSCode:

  • Find: (import .* from ".*(?!\.js)(.){3})".
  • Replace: $1.js".

The find expression will match imports without the .js extension. The first group will capture the part up to the closing quote. The replace expression then takes the first group of the match, which always doesn't have the .js extension, and then appends the extension.

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.

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:

fix-esm-import-paths

Test before using in your project.

If you are using VS code, you can use regex to replace the paths.

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

Replace: $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. Example:

import fs from 'fs'

Supports multi-line imports. Example:

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

Supports as imports. Example:

import * as foo from './file'

Supports single and double quotes. Example:

import foo from './file'
import foo from "./file"

Supports exports. See export docs. Example:

export { foo } from './file'

npm, anyone?

npm i fix-esm-import-path

check it on npmjs or github.

Only has 8 stars (one is from me), but I'm using it on multiple projects and it does what I need:

npx fix-esm-import-path dist/your-compiled-entrypoint.js