Просмотр исходного кода

fix(compiler-sfc): resolve top-level exports from files registered as global types (#14805)

resolves nuxt/nuxt#33694
Daniel Roe 3 недель назад
Родитель
Сommit
3d077f26e3

+ 59 - 0
packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts

@@ -1733,6 +1733,65 @@ describe('resolveType', () => {
       })
     })
 
+    test('global type file with top-level exports is also importable', () => {
+      const files = {
+        '/globalTypes.d.ts': `export interface Foo { bar: string }`,
+      }
+      const { props, deps } = resolve(
+        `
+        import type { Foo } from './globalTypes'
+        defineProps<Foo>()
+        `,
+        files,
+        { globalTypeFiles: ['/globalTypes.d.ts'] },
+      )
+      expect(props).toStrictEqual({
+        bar: ['String'],
+      })
+      expect(deps && [...deps]).toContain('/globalTypes.d.ts')
+    })
+
+    test(
+      'global type file with top-level exports is importable ' +
+        'after global-scope load',
+      () => {
+        const files = {
+          '/globalTypes.d.ts': `export interface Foo { bar: string }`,
+        }
+        // Reference an unknown name in the first compilation so that
+        // resolution falls back to the global scope, which loads
+        // `/globalTypes.d.ts` with `asGlobal=true` and caches the result.
+        // This mirrors the dev-server order in which one SFC triggers
+        // global-scope loading before another SFC imports from the
+        // same file. The compile itself fails on the unknown reference,
+        // but the cache is populated as a side effect before that error
+        // is thrown.
+        try {
+          resolve(
+            `defineProps<UnknownGlobal>()`,
+            files,
+            { globalTypeFiles: ['/globalTypes.d.ts'] },
+            '/PrimeGlobal.vue',
+          )
+        } catch {}
+
+        const { props, deps } = resolve(
+          `
+          import type { Foo } from './globalTypes'
+          defineProps<Foo>()
+          `,
+          files,
+          { globalTypeFiles: ['/globalTypes.d.ts'] },
+          '/Importer.vue',
+          false /* do not invalidate cache */,
+        )
+        expect(props).toStrictEqual({
+          bar: ['String'],
+        })
+        expect(deps && [...deps]).toContain('/globalTypes.d.ts')
+      },
+    )
+
     // #9871
     test('shared generics with different args', () => {
       const files = {

+ 9 - 2
packages/compiler-sfc/src/script/resolveType.ts

@@ -1193,7 +1193,12 @@ function loadTSConfig(
   return res
 }
 
+// `recordTypes` records different members onto the TypeScope depending on
+// `asGlobal`, so the two contexts must not share a cache entry — otherwise a
+// global-scope load can poison a later import-based resolution of the same
+// file.
 const fileToScopeCache = createCache<TypeScope>()
+const fileToGlobalScopeCache = createCache<TypeScope>()
 
 /**
  * @private
@@ -1201,6 +1206,7 @@ const fileToScopeCache = createCache<TypeScope>()
 export function invalidateTypeCache(filename: string): void {
   filename = normalizePath(filename)
   fileToScopeCache.delete(filename)
+  fileToGlobalScopeCache.delete(filename)
   tsConfigCache.delete(filename)
   const affectedConfig = tsConfigRefMap.get(filename)
   if (affectedConfig) tsConfigCache.delete(affectedConfig)
@@ -1211,7 +1217,8 @@ export function fileToScope(
   filename: string,
   asGlobal = false,
 ): TypeScope {
-  const cached = fileToScopeCache.get(filename)
+  const cache = asGlobal ? fileToGlobalScopeCache : fileToScopeCache
+  const cached = cache.get(filename)
   if (cached) {
     return cached
   }
@@ -1221,7 +1228,7 @@ export function fileToScope(
   const body = parseFile(filename, source, fs, ctx.options.babelParserPlugins)
   const scope = new TypeScope(filename, source, 0, recordImports(body))
   recordTypes(ctx, body, scope, asGlobal)
-  fileToScopeCache.set(filename, scope)
+  cache.set(filename, scope)
   return scope
 }