bar: ['String'],
})
})
+
+ // https://github.com/vuejs/router/issues/2611
+ test('modular js extension', () => {
+ const files = {
+ '/mts.mjs': 'export {}',
+ '/mts.d.mts': 'export type LinkProps = { activeClass: string }',
+ '/tsx.jsx': 'export {}',
+ '/tsx.d.ts': 'export type Foo = number',
+ '/mtsTyped.mjs': 'export {}',
+ '/mtsTyped.d.ts': 'export type Bar = string',
+ '/cts.cjs': 'module.exports = {}',
+ '/cts.d.cts': `export type Baz = boolean`,
+ }
+
+ let props!: Record<string, string[]>
+ expect(() => {
+ props = resolve(
+ `
+ import type { LinkProps } from './mts.mjs'
+ import { Foo } from './tsx.jsx'
+ import { Bar } from './mtsTyped.mjs'
+ import type { Baz } from './cts.cjs'
+ defineProps<LinkProps & { foo: Foo; bar: Bar; baz: Baz }>()
+ `,
+ files,
+ ).props
+ }).not.toThrow()
+ expect(props).not.toBe(undefined)
+ expect(props).toStrictEqual({
+ foo: ['Number'],
+ bar: ['String'],
+ baz: ['Boolean'],
+ activeClass: ['String'],
+ })
+ })
+
+ test('prefer .mts over .ts for .mjs import', () => {
+ const files = {
+ '/foo.mjs': 'export {}',
+ '/foo.ts': 'export type Foo = number',
+ '/foo.mts': 'export type Foo = string',
+ }
+
+ const { props } = resolve(
+ `
+ import type { Foo } from './foo.mjs'
+ defineProps<{ value: Foo }>()
+ `,
+ files,
+ )
+
+ expect(props).toStrictEqual({
+ value: ['String'],
+ })
+ })
+
+ test('prefer .d.mts over .d.ts for .mjs import', () => {
+ const files = {
+ '/foo.mjs': 'export {}',
+ '/foo.d.ts': 'export type Foo = number',
+ '/foo.d.mts': 'export type Foo = string',
+ }
+
+ const { props } = resolve(
+ `
+ import type { Foo } from './foo.mjs'
+ defineProps<{ value: Foo }>()
+ `,
+ files,
+ )
+
+ expect(props).toStrictEqual({
+ value: ['String'],
+ })
+ })
+
+ test('prefer .d.cts over .d.ts for .cjs import', () => {
+ const files = {
+ '/foo.cjs': 'module.exports = {}',
+ '/foo.d.ts': 'export type Foo = number',
+ '/foo.d.cts': 'export type Foo = boolean',
+ }
+
+ const { props } = resolve(
+ `
+ import type { Foo } from './foo.cjs'
+ defineProps<{ value: Foo }>()
+ `,
+ files,
+ )
+
+ expect(props).toStrictEqual({
+ value: ['Boolean'],
+ })
+ })
})
})
}
function resolveExt(filename: string, fs: FS) {
+ // Keep the import's module kind so we can mirror TS NodeNext fallback order.
+ let moduleType: /*cjs*/ 'c' | /*mjs*/ 'm' | /*unknown*/ 'u' = 'u'
+ if (filename.endsWith('.mjs')) {
+ moduleType = 'm'
+ } else if (filename.endsWith('.cjs')) {
+ moduleType = 'c'
+ }
// #8339 ts may import .js but we should resolve to corresponding ts or d.ts
- filename = filename.replace(/\.js$/, '')
+ filename = filename.replace(/\.[cm]?jsx?$/, '')
const tryResolve = (filename: string) => {
if (fs.fileExists(filename)) return filename
}
- return (
- tryResolve(filename) ||
+ const resolveTs = () =>
tryResolve(filename + `.ts`) ||
tryResolve(filename + `.tsx`) ||
- tryResolve(filename + `.d.ts`) ||
+ tryResolve(filename + `.d.ts`)
+ const resolveMts = () =>
+ tryResolve(filename + `.mts`) || tryResolve(filename + `.d.mts`)
+ const resolveCts = () =>
+ tryResolve(filename + `.cts`) || tryResolve(filename + `.d.cts`)
+
+ return (
+ tryResolve(filename) ||
+ // For explicit .mjs/.cjs imports, prefer .mts/.cts declarations first.
+ (moduleType === 'm'
+ ? resolveMts() || resolveTs()
+ : moduleType === 'c'
+ ? resolveCts() || resolveTs()
+ : resolveTs() || resolveMts() || resolveCts()) ||
tryResolve(joinPaths(filename, `index.ts`)) ||
tryResolve(joinPaths(filename, `index.tsx`)) ||
tryResolve(joinPaths(filename, `index.d.ts`))
parserPlugins?: SFCScriptCompileOptions['babelParserPlugins'],
): Statement[] {
const ext = extname(filename)
- if (ext === '.ts' || ext === '.mts' || ext === '.tsx' || ext === '.mtsx') {
+ if (
+ ext === '.ts' ||
+ ext === '.mts' ||
+ ext === '.tsx' ||
+ ext === '.cts' ||
+ ext === '.mtsx'
+ ) {
return babelParse(content, {
plugins: resolveParserPlugins(
ext.slice(1),
parserPlugins,
- /\.d\.m?ts$/.test(filename),
+ /\.d\.[cm]?ts$/.test(filename),
),
sourceType: 'module',
}).program.body