diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 5b6fd6c59e9f72..4c3f72457b7766 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -25,7 +25,10 @@ import { cleanUrl, slash, nestedResolveFrom, - isFileReadable + isFileReadable, + isTsRequest, + isPossibleTsOutput, + getTsSrcPath } from '../utils' import { ViteDevServer, SSROptions } from '..' import { createFilter } from '@rollup/pluginutils' @@ -65,6 +68,11 @@ export interface InternalResolveOptions extends ResolveOptions { skipPackageJson?: boolean preferRelative?: boolean isRequire?: boolean + // #3040 + // when the importer is a ts module, + // if the specifier requests a non-existent `.js/jsx/mjs/cjs` file, + // should also try import from `.ts/tsx/mts/cts` source file as fallback, + isFromTsImporter?: boolean } export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin { @@ -75,10 +83,6 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin { ssrConfig, preferRelative = false } = baseOptions - const requireOptions: InternalResolveOptions = { - ...baseOptions, - isRequire: true - } let server: ViteDevServer | undefined const { target: ssrTarget, noExternal: ssrNoExternal } = ssrConfig ?? {} @@ -104,13 +108,19 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin { const targetWeb = !ssr || ssrTarget === 'webworker' // this is passed by @rollup/plugin-commonjs - const isRequire = + const isRequire = !!( resolveOpts && resolveOpts.custom && resolveOpts.custom['node-resolve'] && resolveOpts.custom['node-resolve'].isRequire + ) - const options = isRequire ? requireOptions : baseOptions + const options: InternalResolveOptions = { + ...baseOptions, + + isRequire, + isFromTsImporter: isTsRequest(importer ?? '') + } const preserveSymlinks = !!server?.config.resolve.preserveSymlinks @@ -450,6 +460,22 @@ function tryResolveFile( if (index) return index + postfix } } + + const tryTsExtension = options.isFromTsImporter && isPossibleTsOutput(file) + if (tryTsExtension) { + const tsSrcPath = getTsSrcPath(file) + return tryResolveFile( + tsSrcPath, + postfix, + options, + tryIndex, + targetWeb, + preserveSymlinks, + tryPrefix, + skipPackageJson + ) + } + if (tryPrefix) { const prefixed = `${path.dirname(file)}/${tryPrefix}${path.basename(file)}` return tryResolveFile( diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 4d6d4436b4248b..bdbbce4faa166e 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -164,6 +164,14 @@ export const isJSRequest = (url: string): boolean => { return false } +const knownTsRE = /\.(ts|mts|cts|tsx)$/ +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') + const importQueryRE = /(\?|&)import=?(?:&|$)/ const internalPrefixes = [ FS_PREFIX,