diff --git a/packages/playground/resolve/index.html b/packages/playground/resolve/index.html
index db0a4bc54f1ad7..100fd60ff653c1 100644
--- a/packages/playground/resolve/index.html
+++ b/packages/playground/resolve/index.html
@@ -38,6 +38,17 @@
fail
+
+ A ts module can import another tsx module using its corresponding jsx file
+ name
+
+fail
+
+
+ A ts module can import another tsx module using its corresponding js file name
+
+fail
+
Resolve file name containing dot
fail
@@ -130,6 +141,12 @@ resolve package that contains # in path
import { msg as tsExtensionMsg } from './ts-extension'
text('.ts-extension', tsExtensionMsg)
+ import { msgJsx as tsJsxExtensionMsg } from './ts-extension'
+ text('.jsx-extension', tsJsxExtensionMsg)
+
+ import { msgTsx as tsTsxExtensionMsg } from './ts-extension'
+ text('.tsx-extension', tsTsxExtensionMsg)
+
// filename with dot
import { bar } from './util/bar.util'
text('.dot', bar())
diff --git a/packages/playground/resolve/ts-extension/hellojsx.tsx b/packages/playground/resolve/ts-extension/hellojsx.tsx
new file mode 100644
index 00000000000000..a8f610b399b17a
--- /dev/null
+++ b/packages/playground/resolve/ts-extension/hellojsx.tsx
@@ -0,0 +1 @@
+export const msgJsx = '[success] use .jsx extension to import a tsx module'
diff --git a/packages/playground/resolve/ts-extension/hellotsx.tsx b/packages/playground/resolve/ts-extension/hellotsx.tsx
new file mode 100644
index 00000000000000..b7461ca71ded6c
--- /dev/null
+++ b/packages/playground/resolve/ts-extension/hellotsx.tsx
@@ -0,0 +1 @@
+export const msgTsx = '[success] use .js extension to import a tsx module'
diff --git a/packages/playground/resolve/ts-extension/index.ts b/packages/playground/resolve/ts-extension/index.ts
index e095619ee4d716..bdb326f8778e64 100644
--- a/packages/playground/resolve/ts-extension/index.ts
+++ b/packages/playground/resolve/ts-extension/index.ts
@@ -1,3 +1,5 @@
import { msg } from './hello.js'
+import { msgJsx } from './hellojsx.jsx'
+import { msgTsx } from './hellotsx.js'
-export { msg }
+export { msg, msgJsx, msgTsx }
diff --git a/packages/vite/src/node/__tests__/utils.spec.ts b/packages/vite/src/node/__tests__/utils.spec.ts
index 3bf82f6b86f84b..26b149cd48b8af 100644
--- a/packages/vite/src/node/__tests__/utils.spec.ts
+++ b/packages/vite/src/node/__tests__/utils.spec.ts
@@ -1,4 +1,4 @@
-import { injectQuery, isWindows } from '../utils'
+import { getPotentialTsSrcPaths, injectQuery, isWindows } from '../utils'
if (isWindows) {
// this test will work incorrectly on unix systems
@@ -38,3 +38,39 @@ test('path with unicode, space, and %', () => {
'/usr/vite/東京 %20 hello?direct'
)
})
+
+test('ts import of file with .js extension', () => {
+ expect(getPotentialTsSrcPaths('test-file.js')).toEqual([
+ 'test-file.ts',
+ 'test-file.tsx'
+ ])
+})
+
+test('ts import of file with .jsx extension', () => {
+ expect(getPotentialTsSrcPaths('test-file.jsx')).toEqual(['test-file.tsx'])
+})
+
+test('ts import of file .mjs,.cjs extension', () => {
+ expect(getPotentialTsSrcPaths('test-file.cjs')).toEqual([
+ 'test-file.cts',
+ 'test-file.ctsx'
+ ])
+ expect(getPotentialTsSrcPaths('test-file.mjs')).toEqual([
+ 'test-file.mts',
+ 'test-file.mtsx'
+ ])
+})
+
+test('ts import of file with .js before extension', () => {
+ expect(getPotentialTsSrcPaths('test-file.js.js')).toEqual([
+ 'test-file.js.ts',
+ 'test-file.js.tsx'
+ ])
+})
+
+test('ts import of file with .js and query param', () => {
+ expect(getPotentialTsSrcPaths('test-file.js.js?lee=123')).toEqual([
+ 'test-file.js.ts?lee=123',
+ 'test-file.js.tsx?lee=123'
+ ])
+})
diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts
index 55afd92df1a8fb..1dacf4ab15a11d 100644
--- a/packages/vite/src/node/plugins/resolve.ts
+++ b/packages/vite/src/node/plugins/resolve.ts
@@ -27,7 +27,7 @@ import {
isFileReadable,
isTsRequest,
isPossibleTsOutput,
- getTsSrcPath
+ getPotentialTsSrcPaths
} from '../utils'
import type { ViteDevServer, SSROptions } from '..'
import type { PartialResolvedId } from 'rollup'
@@ -436,16 +436,20 @@ function tryResolveFile(
const tryTsExtension = options.isFromTsImporter && isPossibleTsOutput(file)
if (tryTsExtension) {
- const tsSrcPath = getTsSrcPath(file)
- return tryResolveFile(
- tsSrcPath,
- postfix,
- options,
- tryIndex,
- targetWeb,
- tryPrefix,
- skipPackageJson
- )
+ const tsSrcPaths = getPotentialTsSrcPaths(file)
+ for (const srcPath of tsSrcPaths) {
+ const res = tryResolveFile(
+ srcPath,
+ postfix,
+ options,
+ tryIndex,
+ targetWeb,
+ tryPrefix,
+ skipPackageJson
+ )
+ if (res) return res
+ }
+ return
}
if (tryPrefix) {
diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts
index 3012e6d57e6c65..4fdae99f268bf8 100644
--- a/packages/vite/src/node/utils.ts
+++ b/packages/vite/src/node/utils.ts
@@ -189,8 +189,14 @@ const knownTsOutputRE = /\.(js|mjs|cjs|jsx)$/
export const isTsRequest = (url: string) => knownTsRE.test(cleanUrl(url))
export const isPossibleTsOutput = (url: string) =>
knownTsOutputRE.test(cleanUrl(url))
-export const getTsSrcPath = (filename: string) =>
- filename.replace(/\.([cm])?(js)(x?)(\?|$)/, '.$1ts$3')
+export function getPotentialTsSrcPaths(filePath: string) {
+ const [name, type, query = ''] = filePath.split(/(\.(?:[cm]?js|jsx))(\?.*)?$/)
+ const paths = [name + type.replace('js', 'ts') + query]
+ if (!type.endsWith('x')) {
+ paths.push(name + type.replace('js', 'tsx') + query)
+ }
+ return paths
+}
const importQueryRE = /(\?|&)import=?(?:&|$)/
const internalPrefixes = [