]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-sfc): add resolution trying for `.mts/.cts` files (#14402)
authorrg <74761884+Gehbt@users.noreply.github.com>
Mon, 9 Feb 2026 07:52:58 +0000 (15:52 +0800)
committerGitHub <noreply@github.com>
Mon, 9 Feb 2026 07:52:58 +0000 (15:52 +0800)
Closes vuejs/router#2611

packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
packages/compiler-sfc/src/script/context.ts
packages/compiler-sfc/src/script/resolveType.ts

index a24a21c23cb66367855a7afcfab73486ae00eb7d..dd03cb973b8718f66e49fc71c67084aef8648905 100644 (file)
@@ -1692,6 +1692,101 @@ describe('resolveType', () => {
         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'],
+      })
+    })
   })
 })
 
index 3d6a129161f154e065e78ac9a491997b29a653aa..7b47839418e485fec0718d002dbb538dcb000342 100644 (file)
@@ -188,7 +188,13 @@ export function resolveParserPlugins(
     // should remove the jsx from user options
     userPlugins = userPlugins.filter(p => p !== 'jsx')
   }
-  if (lang === 'ts' || lang === 'mts' || lang === 'tsx' || lang === 'mtsx') {
+  if (
+    lang === 'ts' ||
+    lang === 'mts' ||
+    lang === 'tsx' ||
+    lang === 'cts' ||
+    lang === 'mtsx'
+  ) {
     plugins.push(['typescript', { dts }], 'explicitResourceManagement')
     if (!userPlugins || !userPlugins.includes('decorators')) {
       plugins.push('decorators-legacy')
index 393fd1a4e4189a8a4b5cd10556141ba75a251c3e..14e5fabe5eb604c149f4b41ce4f12e2a7cd1cb93 100644 (file)
@@ -986,16 +986,35 @@ function importSourceToScope(
 }
 
 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`))
@@ -1178,12 +1197,18 @@ function parseFile(
   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