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

fix(compiler-sfc): resolve type re-exports inside declare global (#14766)

Daniel Roe 1 месяц назад
Родитель
Сommit
acfffe34e7

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

@@ -1497,6 +1497,139 @@ describe('resolveType', () => {
       })
     })
 
+    test('global types with re-exports', () => {
+      const files = {
+        '/foo.ts': `export interface Foo { foo: number }`,
+        '/bar.ts': `export interface Bar { bar: boolean }`,
+        '/baz.ts': `export interface Baz { baz: string }`,
+        '/global.d.ts': `
+          declare global {
+            export type { Foo } from './foo'
+            export { Bar } from './bar'
+            export * from './baz'
+          }
+          export {}
+        `,
+      }
+
+      const globalTypeFiles = { globalTypeFiles: ['/global.d.ts'] }
+
+      const fooRes = resolve(`defineProps<Foo>()`, files, globalTypeFiles)
+      expect(fooRes.props).toStrictEqual({
+        foo: ['Number'],
+      })
+      expect(fooRes.deps && [...fooRes.deps]).toStrictEqual([
+        '/global.d.ts',
+        '/foo.ts',
+      ])
+
+      const barRes = resolve(`defineProps<Bar>()`, files, globalTypeFiles)
+      expect(barRes.props).toStrictEqual({
+        bar: ['Boolean'],
+      })
+      expect(barRes.deps && [...barRes.deps]).toStrictEqual([
+        '/global.d.ts',
+        '/bar.ts',
+      ])
+
+      const bazRes = resolve(`defineProps<Baz>()`, files, globalTypeFiles)
+      expect(bazRes.props).toStrictEqual({
+        baz: ['String'],
+      })
+      expect(bazRes.deps && [...bazRes.deps]).toStrictEqual([
+        '/global.d.ts',
+        '/baz.ts',
+      ])
+    })
+
+    test('global types with re-exports preserve source-module scope', () => {
+      const files = {
+        '/types.ts': `export type Name = string`,
+        '/foo.ts': `
+          import type { Name } from './types'
+          export interface Foo { name: Name }
+        `,
+        '/global.d.ts': `
+          declare global {
+            export type { Foo } from './foo'
+          }
+          export {}
+        `,
+      }
+
+      expect(
+        resolve(`defineProps<Foo>()`, files, {
+          globalTypeFiles: ['/global.d.ts'],
+        }).props,
+      ).toStrictEqual({
+        name: ['String'],
+      })
+    })
+
+    test('global types with re-exports from package directory', () => {
+      const files = {
+        '/node_modules/pkg/package.json': `{ "types": "dist/index.d.ts" }`,
+        '/node_modules/pkg/dist/index.d.ts': `
+          export interface PackageType { value: string }
+        `,
+        '/global.d.ts': `
+          declare global {
+            export type { PackageType } from './node_modules/pkg'
+          }
+          export {}
+        `,
+      }
+
+      const { props, deps } = resolve(`defineProps<PackageType>()`, files, {
+        globalTypeFiles: ['/global.d.ts'],
+      })
+
+      expect(props).toStrictEqual({
+        value: ['String'],
+      })
+      expect(deps && [...deps]).toStrictEqual([
+        '/global.d.ts',
+        '/node_modules/pkg/dist/index.d.ts',
+      ])
+    })
+
+    test('global types with re-exports track source deps after cache reuse', () => {
+      const files = {
+        '/base.ts': `export interface Base { age: number }`,
+        '/types.ts': `export type Name = string`,
+        '/foo.ts': `
+          import type { Base } from './base'
+          import type { Name } from './types'
+          export interface Foo extends Base { name: Name }
+        `,
+        '/global.d.ts': `
+          declare global {
+            export type { Foo } from './foo'
+          }
+          export {}
+        `,
+      }
+      const globalTypeFiles = { globalTypeFiles: ['/global.d.ts'] }
+      const expectedDeps = ['/global.d.ts', '/foo.ts', '/base.ts', '/types.ts']
+
+      const { deps: coldDeps } = resolve(
+        `defineProps<Foo>()`,
+        files,
+        globalTypeFiles,
+      )
+      expect(coldDeps && [...coldDeps]).toStrictEqual(expectedDeps)
+
+      const { deps } = resolve(
+        `defineProps<Foo>()`,
+        files,
+        globalTypeFiles,
+        '/Other.vue',
+        false,
+      )
+
+      expect(deps && [...deps]).toStrictEqual(expectedDeps)
+    })
+
     test('global types with ambient references', () => {
       const files = {
         // with references

+ 79 - 8
packages/compiler-sfc/src/script/resolveType.ts

@@ -143,6 +143,24 @@ interface ResolvedElements {
   calls?: (TSCallSignatureDeclaration | TSFunctionType)[]
 }
 
+function recordScopeDep(
+  ctx: TypeResolveContext,
+  scope: TypeScope | undefined,
+): void {
+  if (scope && scope.filename !== ctx.filename) {
+    ;(ctx.deps || (ctx.deps = new Set())).add(scope.filename)
+  }
+}
+
+function recordResolvedElementDeps(
+  ctx: TypeResolveContext,
+  { props }: ResolvedElements,
+): void {
+  for (const key in props) {
+    recordScopeDep(ctx, props[key]._ownerScope)
+  }
+}
+
 /**
  * Resolve arbitrary type node to a list of type elements that can be then
  * mapped to runtime props or emits.
@@ -155,6 +173,7 @@ export function resolveTypeElements(
 ): ResolvedElements {
   const canCache = !typeParameters
   if (canCache && node._resolvedElements) {
+    recordResolvedElementDeps(ctx, node._resolvedElements)
     return node._resolvedElements
   }
   const resolved = innerResolveTypeElements(
@@ -738,6 +757,7 @@ function resolveTypeReference(
 ): ScopeTypeNode | undefined {
   const canCache = !scope?.isGenericScope
   if (canCache && node._resolvedReference) {
+    recordScopeDep(ctx, node._resolvedReference._ownerScope)
     return node._resolvedReference
   }
   const resolved = innerResolveTypeReference(
@@ -779,7 +799,11 @@ function innerResolveTypeReference(
             const src = node.type === 'TSTypeQuery' ? s.declares : s.types
             if (src[name]) {
               ;(ctx.deps || (ctx.deps = new Set())).add(s.filename)
-              return src[name]
+              const resolved = src[name]
+              if (resolved._ownerScope && resolved._ownerScope !== s) {
+                ctx.deps.add(resolved._ownerScope.filename)
+              }
+              return resolved
             }
           }
         }
@@ -918,6 +942,7 @@ function importSourceToScope(
   node: Node,
   scope: TypeScope,
   source: string,
+  trackDep = true,
 ): TypeScope {
   let fs: FS | undefined
   try {
@@ -968,13 +993,23 @@ function importSourceToScope(
       }
       resolved = resolveWithTS(scope.filename, source, ts, fs)
     }
+    if (!resolved && source[0] === '.' && __CJS__) {
+      if (!ts) {
+        if (loadTS) ts = loadTS()
+      }
+      if (ts) {
+        resolved = resolveWithTS(scope.filename, source, ts, fs)
+      }
+    }
     if (resolved) {
       resolved = scope.resolvedImportSources[source] = normalizePath(resolved)
     }
   }
   if (resolved) {
     // (hmr) register dependency file on ctx
-    ;(ctx.deps || (ctx.deps = new Set())).add(resolved)
+    if (trackDep) {
+      ;(ctx.deps || (ctx.deps = new Set())).add(resolved)
+    }
     return fileToScope(ctx, resolved)
   } else {
     return ctx.error(
@@ -1337,9 +1372,45 @@ function recordTypes(
         }
       } else if (stmt.type === 'TSModuleDeclaration' && stmt.global) {
         for (const s of (stmt.body as TSModuleBlock).body) {
-          if (s.type === 'ExportNamedDeclaration' && s.declaration) {
-            // Handle export declarations inside declare global
-            recordType(s.declaration, types, declares)
+          if (s.type === 'ExportNamedDeclaration') {
+            if (s.declaration) {
+              // Handle export declarations inside declare global
+              recordType(s.declaration, types, declares)
+            } else if (s.source) {
+              // Handle re-exports inside declare global, e.g.
+              // `export type { Foo } from './foo'`. Global lookup only checks
+              // `types`/`declares`, so resolve the source eagerly.
+              const sourceScope = importSourceToScope(
+                ctx,
+                s.source,
+                scope,
+                s.source.value,
+                false,
+              )
+              for (const spec of s.specifiers) {
+                if (spec.type === 'ExportSpecifier') {
+                  const exported = getId(spec.exported)
+                  const local = spec.local.name
+                  if (sourceScope.exportedTypes[local]) {
+                    types[exported] = sourceScope.exportedTypes[local]
+                  }
+                  if (sourceScope.exportedDeclares[local]) {
+                    declares[exported] = sourceScope.exportedDeclares[local]
+                  }
+                }
+              }
+            }
+          } else if (s.type === 'ExportAllDeclaration' && s.source) {
+            // Handle `export * from './foo'` inside declare global
+            const sourceScope = importSourceToScope(
+              ctx,
+              s.source,
+              scope,
+              s.source.value,
+              false,
+            )
+            Object.assign(types, sourceScope.exportedTypes)
+            Object.assign(declares, sourceScope.exportedDeclares)
           } else {
             recordType(s, types, declares)
           }
@@ -1406,11 +1477,11 @@ function recordTypes(
   }
   for (const key of Object.keys(types)) {
     const node = types[key]
-    node._ownerScope = scope
-    if (node._ns) node._ns._ownerScope = scope
+    if (!node._ownerScope) node._ownerScope = scope
+    if (node._ns && !node._ns._ownerScope) node._ns._ownerScope = scope
   }
   for (const key of Object.keys(declares)) {
-    declares[key]._ownerScope = scope
+    if (!declares[key]._ownerScope) declares[key]._ownerScope = scope
   }
 }