Skip to content

Commit acfffe3

Browse files
authored
fix(compiler-sfc): resolve type re-exports inside declare global (#14766)
1 parent a037385 commit acfffe3

2 files changed

Lines changed: 212 additions & 8 deletions

File tree

packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1497,6 +1497,139 @@ describe('resolveType', () => {
14971497
})
14981498
})
14991499

1500+
test('global types with re-exports', () => {
1501+
const files = {
1502+
'/foo.ts': `export interface Foo { foo: number }`,
1503+
'/bar.ts': `export interface Bar { bar: boolean }`,
1504+
'/baz.ts': `export interface Baz { baz: string }`,
1505+
'/global.d.ts': `
1506+
declare global {
1507+
export type { Foo } from './foo'
1508+
export { Bar } from './bar'
1509+
export * from './baz'
1510+
}
1511+
export {}
1512+
`,
1513+
}
1514+
1515+
const globalTypeFiles = { globalTypeFiles: ['/global.d.ts'] }
1516+
1517+
const fooRes = resolve(`defineProps<Foo>()`, files, globalTypeFiles)
1518+
expect(fooRes.props).toStrictEqual({
1519+
foo: ['Number'],
1520+
})
1521+
expect(fooRes.deps && [...fooRes.deps]).toStrictEqual([
1522+
'/global.d.ts',
1523+
'/foo.ts',
1524+
])
1525+
1526+
const barRes = resolve(`defineProps<Bar>()`, files, globalTypeFiles)
1527+
expect(barRes.props).toStrictEqual({
1528+
bar: ['Boolean'],
1529+
})
1530+
expect(barRes.deps && [...barRes.deps]).toStrictEqual([
1531+
'/global.d.ts',
1532+
'/bar.ts',
1533+
])
1534+
1535+
const bazRes = resolve(`defineProps<Baz>()`, files, globalTypeFiles)
1536+
expect(bazRes.props).toStrictEqual({
1537+
baz: ['String'],
1538+
})
1539+
expect(bazRes.deps && [...bazRes.deps]).toStrictEqual([
1540+
'/global.d.ts',
1541+
'/baz.ts',
1542+
])
1543+
})
1544+
1545+
test('global types with re-exports preserve source-module scope', () => {
1546+
const files = {
1547+
'/types.ts': `export type Name = string`,
1548+
'/foo.ts': `
1549+
import type { Name } from './types'
1550+
export interface Foo { name: Name }
1551+
`,
1552+
'/global.d.ts': `
1553+
declare global {
1554+
export type { Foo } from './foo'
1555+
}
1556+
export {}
1557+
`,
1558+
}
1559+
1560+
expect(
1561+
resolve(`defineProps<Foo>()`, files, {
1562+
globalTypeFiles: ['/global.d.ts'],
1563+
}).props,
1564+
).toStrictEqual({
1565+
name: ['String'],
1566+
})
1567+
})
1568+
1569+
test('global types with re-exports from package directory', () => {
1570+
const files = {
1571+
'/node_modules/pkg/package.json': `{ "types": "dist/index.d.ts" }`,
1572+
'/node_modules/pkg/dist/index.d.ts': `
1573+
export interface PackageType { value: string }
1574+
`,
1575+
'/global.d.ts': `
1576+
declare global {
1577+
export type { PackageType } from './node_modules/pkg'
1578+
}
1579+
export {}
1580+
`,
1581+
}
1582+
1583+
const { props, deps } = resolve(`defineProps<PackageType>()`, files, {
1584+
globalTypeFiles: ['/global.d.ts'],
1585+
})
1586+
1587+
expect(props).toStrictEqual({
1588+
value: ['String'],
1589+
})
1590+
expect(deps && [...deps]).toStrictEqual([
1591+
'/global.d.ts',
1592+
'/node_modules/pkg/dist/index.d.ts',
1593+
])
1594+
})
1595+
1596+
test('global types with re-exports track source deps after cache reuse', () => {
1597+
const files = {
1598+
'/base.ts': `export interface Base { age: number }`,
1599+
'/types.ts': `export type Name = string`,
1600+
'/foo.ts': `
1601+
import type { Base } from './base'
1602+
import type { Name } from './types'
1603+
export interface Foo extends Base { name: Name }
1604+
`,
1605+
'/global.d.ts': `
1606+
declare global {
1607+
export type { Foo } from './foo'
1608+
}
1609+
export {}
1610+
`,
1611+
}
1612+
const globalTypeFiles = { globalTypeFiles: ['/global.d.ts'] }
1613+
const expectedDeps = ['/global.d.ts', '/foo.ts', '/base.ts', '/types.ts']
1614+
1615+
const { deps: coldDeps } = resolve(
1616+
`defineProps<Foo>()`,
1617+
files,
1618+
globalTypeFiles,
1619+
)
1620+
expect(coldDeps && [...coldDeps]).toStrictEqual(expectedDeps)
1621+
1622+
const { deps } = resolve(
1623+
`defineProps<Foo>()`,
1624+
files,
1625+
globalTypeFiles,
1626+
'/Other.vue',
1627+
false,
1628+
)
1629+
1630+
expect(deps && [...deps]).toStrictEqual(expectedDeps)
1631+
})
1632+
15001633
test('global types with ambient references', () => {
15011634
const files = {
15021635
// with references

packages/compiler-sfc/src/script/resolveType.ts

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,24 @@ interface ResolvedElements {
143143
calls?: (TSCallSignatureDeclaration | TSFunctionType)[]
144144
}
145145

146+
function recordScopeDep(
147+
ctx: TypeResolveContext,
148+
scope: TypeScope | undefined,
149+
): void {
150+
if (scope && scope.filename !== ctx.filename) {
151+
;(ctx.deps || (ctx.deps = new Set())).add(scope.filename)
152+
}
153+
}
154+
155+
function recordResolvedElementDeps(
156+
ctx: TypeResolveContext,
157+
{ props }: ResolvedElements,
158+
): void {
159+
for (const key in props) {
160+
recordScopeDep(ctx, props[key]._ownerScope)
161+
}
162+
}
163+
146164
/**
147165
* Resolve arbitrary type node to a list of type elements that can be then
148166
* mapped to runtime props or emits.
@@ -155,6 +173,7 @@ export function resolveTypeElements(
155173
): ResolvedElements {
156174
const canCache = !typeParameters
157175
if (canCache && node._resolvedElements) {
176+
recordResolvedElementDeps(ctx, node._resolvedElements)
158177
return node._resolvedElements
159178
}
160179
const resolved = innerResolveTypeElements(
@@ -738,6 +757,7 @@ function resolveTypeReference(
738757
): ScopeTypeNode | undefined {
739758
const canCache = !scope?.isGenericScope
740759
if (canCache && node._resolvedReference) {
760+
recordScopeDep(ctx, node._resolvedReference._ownerScope)
741761
return node._resolvedReference
742762
}
743763
const resolved = innerResolveTypeReference(
@@ -779,7 +799,11 @@ function innerResolveTypeReference(
779799
const src = node.type === 'TSTypeQuery' ? s.declares : s.types
780800
if (src[name]) {
781801
;(ctx.deps || (ctx.deps = new Set())).add(s.filename)
782-
return src[name]
802+
const resolved = src[name]
803+
if (resolved._ownerScope && resolved._ownerScope !== s) {
804+
ctx.deps.add(resolved._ownerScope.filename)
805+
}
806+
return resolved
783807
}
784808
}
785809
}
@@ -918,6 +942,7 @@ function importSourceToScope(
918942
node: Node,
919943
scope: TypeScope,
920944
source: string,
945+
trackDep = true,
921946
): TypeScope {
922947
let fs: FS | undefined
923948
try {
@@ -968,13 +993,23 @@ function importSourceToScope(
968993
}
969994
resolved = resolveWithTS(scope.filename, source, ts, fs)
970995
}
996+
if (!resolved && source[0] === '.' && __CJS__) {
997+
if (!ts) {
998+
if (loadTS) ts = loadTS()
999+
}
1000+
if (ts) {
1001+
resolved = resolveWithTS(scope.filename, source, ts, fs)
1002+
}
1003+
}
9711004
if (resolved) {
9721005
resolved = scope.resolvedImportSources[source] = normalizePath(resolved)
9731006
}
9741007
}
9751008
if (resolved) {
9761009
// (hmr) register dependency file on ctx
977-
;(ctx.deps || (ctx.deps = new Set())).add(resolved)
1010+
if (trackDep) {
1011+
;(ctx.deps || (ctx.deps = new Set())).add(resolved)
1012+
}
9781013
return fileToScope(ctx, resolved)
9791014
} else {
9801015
return ctx.error(
@@ -1337,9 +1372,45 @@ function recordTypes(
13371372
}
13381373
} else if (stmt.type === 'TSModuleDeclaration' && stmt.global) {
13391374
for (const s of (stmt.body as TSModuleBlock).body) {
1340-
if (s.type === 'ExportNamedDeclaration' && s.declaration) {
1341-
// Handle export declarations inside declare global
1342-
recordType(s.declaration, types, declares)
1375+
if (s.type === 'ExportNamedDeclaration') {
1376+
if (s.declaration) {
1377+
// Handle export declarations inside declare global
1378+
recordType(s.declaration, types, declares)
1379+
} else if (s.source) {
1380+
// Handle re-exports inside declare global, e.g.
1381+
// `export type { Foo } from './foo'`. Global lookup only checks
1382+
// `types`/`declares`, so resolve the source eagerly.
1383+
const sourceScope = importSourceToScope(
1384+
ctx,
1385+
s.source,
1386+
scope,
1387+
s.source.value,
1388+
false,
1389+
)
1390+
for (const spec of s.specifiers) {
1391+
if (spec.type === 'ExportSpecifier') {
1392+
const exported = getId(spec.exported)
1393+
const local = spec.local.name
1394+
if (sourceScope.exportedTypes[local]) {
1395+
types[exported] = sourceScope.exportedTypes[local]
1396+
}
1397+
if (sourceScope.exportedDeclares[local]) {
1398+
declares[exported] = sourceScope.exportedDeclares[local]
1399+
}
1400+
}
1401+
}
1402+
}
1403+
} else if (s.type === 'ExportAllDeclaration' && s.source) {
1404+
// Handle `export * from './foo'` inside declare global
1405+
const sourceScope = importSourceToScope(
1406+
ctx,
1407+
s.source,
1408+
scope,
1409+
s.source.value,
1410+
false,
1411+
)
1412+
Object.assign(types, sourceScope.exportedTypes)
1413+
Object.assign(declares, sourceScope.exportedDeclares)
13431414
} else {
13441415
recordType(s, types, declares)
13451416
}
@@ -1406,11 +1477,11 @@ function recordTypes(
14061477
}
14071478
for (const key of Object.keys(types)) {
14081479
const node = types[key]
1409-
node._ownerScope = scope
1410-
if (node._ns) node._ns._ownerScope = scope
1480+
if (!node._ownerScope) node._ownerScope = scope
1481+
if (node._ns && !node._ns._ownerScope) node._ns._ownerScope = scope
14111482
}
14121483
for (const key of Object.keys(declares)) {
1413-
declares[key]._ownerScope = scope
1484+
if (!declares[key]._ownerScope) declares[key]._ownerScope = scope
14141485
}
14151486
}
14161487

0 commit comments

Comments
 (0)