Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: importing ts files using their corresponding js extesions #5510

Merged
merged 4 commits into from Nov 2, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/playground/resolve/__tests__/resolve.spec.ts
Expand Up @@ -54,6 +54,10 @@ test('dont add extension to directory name (./dir-with-ext.js/index.js)', async
expect(await page.textContent('.dir-with-ext')).toMatch('[success]')
})

test('a ts module can import another ts module using its corresponding js file name', async () => {
expect(await page.textContent('.ts-extension')).toMatch('[success]')
})

test('filename with dot', async () => {
expect(await page.textContent('.dot')).toMatch('[success]')
})
Expand Down
8 changes: 8 additions & 0 deletions packages/playground/resolve/index.html
Expand Up @@ -33,6 +33,11 @@ <h2>Resolve to non-duplicated file extension</h2>
<h2>Don't add extensions to directory names</h2>
<p class="dir-with-ext">fail</p>

<h2>
A ts module can import another ts module using its corresponding js file name
</h2>
<p class="ts-extension">fail</p>

<h2>Resolve file name containing dot</h2>
<p class="dot">fail</p>

Expand Down Expand Up @@ -119,6 +124,9 @@ <h2>resolve package that contains # in path</h2>
import { file as dirWithExtMsg } from './dir-with-ext'
text('.dir-with-ext', dirWithExtMsg)

import { msg as tsExtensionMsg } from './ts-extension'
text('.ts-extension', tsExtensionMsg)

// filename with dot
import { bar } from './util/bar.util'
text('.dot', bar())
Expand Down
1 change: 1 addition & 0 deletions packages/playground/resolve/ts-extension/hello.ts
@@ -0,0 +1 @@
export const msg = '[success] use .js extension to import a ts module'
3 changes: 3 additions & 0 deletions packages/playground/resolve/ts-extension/index.ts
@@ -0,0 +1,3 @@
import { msg } from './hello.js'

export { msg }
2 changes: 1 addition & 1 deletion packages/playground/resolve/vite.config.js
Expand Up @@ -2,7 +2,7 @@ const virtualFile = '@virtual-file'

module.exports = {
resolve: {
extensions: ['.mjs', '.js', '.es'],
extensions: ['.mjs', '.js', '.es', '.ts'],
mainFields: ['custom', 'module'],
conditions: ['custom']
},
Expand Down
40 changes: 33 additions & 7 deletions packages/vite/src/node/plugins/resolve.ts
Expand Up @@ -25,7 +25,10 @@ import {
cleanUrl,
slash,
nestedResolveFrom,
isFileReadable
isFileReadable,
isTsRequest,
isPossibleTsOutput,
getTsSrcPath
} from '../utils'
import { ViteDevServer, SSROptions } from '..'
import { createFilter } from '@rollup/pluginutils'
Expand Down Expand Up @@ -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 {
Expand All @@ -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 ?? {}
Expand All @@ -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
)
sodatea marked this conversation as resolved.
Show resolved Hide resolved

const options = isRequire ? requireOptions : baseOptions
const options: InternalResolveOptions = {
...baseOptions,

isRequire,
isFromTsImporter: isTsRequest(importer ?? '')
}

const preserveSymlinks = !!server?.config.resolve.preserveSymlinks

Expand Down Expand Up @@ -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(
Expand Down
8 changes: 8 additions & 0 deletions packages/vite/src/node/utils.ts
Expand Up @@ -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,
Expand Down