Skip to content

Commit 21f2803

Browse files
authored
Merge pull request #309 from timocov/star-import-export-issue-304
Fixed handling `import *` statements with local usage but not directly exporting
2 parents 34a18af + eb862bf commit 21f2803

File tree

13 files changed

+217
-49
lines changed

13 files changed

+217
-49
lines changed

src/bundle-generator.ts

+124-32
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
153153

154154
const typesUsageEvaluator = new TypesUsageEvaluator(sourceFiles, typeChecker);
155155

156+
// eslint-disable-next-line complexity
156157
return entries.map((entryConfig: EntryPointConfig) => {
157158
normalLog(`Processing ${entryConfig.filePath}`);
158159

@@ -303,7 +304,10 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
303304

304305
switch (currentModule.type) {
305306
case ModuleType.ShouldBeReferencedAsTypes:
306-
addTypesReference(currentModule.typesLibraryName);
307+
// while a node might be "used" somewhere via transitive nodes
308+
// we need to add types reference only if a node is treated as "should be imported"
309+
// because otherwise we might have lots of false-positive references
310+
forEachNodeThatShouldBeImported(statement, () => addTypesReference(currentModule.typesLibraryName));
307311
break;
308312

309313
case ModuleType.ShouldBeImported:
@@ -590,7 +594,7 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
590594
}
591595
}
592596

593-
function updateImportsForStatement(statement: ts.Statement | ts.SourceFile | ts.ExportSpecifier): void {
597+
function forEachNodeThatShouldBeImported(statement: ts.Statement | ts.SourceFile | ts.ExportSpecifier, callback: (st: ts.DeclarationStatement) => void): void {
594598
const statementsToImport = ts.isVariableStatement(statement)
595599
? statement.declarationList.declarations
596600
: ts.isExportDeclaration(statement) && statement.exportClause !== undefined
@@ -601,25 +605,31 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
601605

602606
for (const statementToImport of statementsToImport) {
603607
if (shouldNodeBeImported(statementToImport as ts.DeclarationStatement)) {
604-
addImport(statementToImport as ts.DeclarationStatement);
605-
606-
// if we're going to add import of any statement in the bundle
607-
// we should check whether the library of that statement
608-
// could be referenced via triple-slash reference-types directive
609-
// because the project which will use bundled declaration file
610-
// can have `types: []` in the tsconfig and it'll fail
611-
// this is especially related to the types packages
612-
// which declares different modules in their declarations
613-
// e.g. @types/node has declaration for "packages" events, fs, path and so on
614-
const sourceFile = statementToImport.getSourceFile();
615-
const moduleInfo = getFileModuleInfo(sourceFile.fileName, criteria);
616-
if (moduleInfo.type === ModuleType.ShouldBeReferencedAsTypes) {
617-
addTypesReference(moduleInfo.typesLibraryName);
618-
}
608+
callback(statementToImport as ts.DeclarationStatement);
619609
}
620610
}
621611
}
622612

613+
function updateImportsForStatement(statement: ts.Statement | ts.SourceFile | ts.ExportSpecifier): void {
614+
forEachNodeThatShouldBeImported(statement, (statementToImport: ts.DeclarationStatement) => {
615+
addImport(statementToImport);
616+
617+
// if we're going to add import of any statement in the bundle
618+
// we should check whether the library of that statement
619+
// could be referenced via triple-slash reference-types directive
620+
// because the project which will use bundled declaration file
621+
// can have `types: []` in the tsconfig and it'll fail
622+
// this is especially related to the types packages
623+
// which declares different modules in their declarations
624+
// e.g. @types/node has declaration for "packages" events, fs, path and so on
625+
const sourceFile = statementToImport.getSourceFile();
626+
const moduleInfo = getFileModuleInfo(sourceFile.fileName, criteria);
627+
if (moduleInfo.type === ModuleType.ShouldBeReferencedAsTypes) {
628+
addTypesReference(moduleInfo.typesLibraryName);
629+
}
630+
});
631+
}
632+
623633
function getDeclarationUsagesSourceFiles(declaration: ts.NamedDeclaration): Set<ts.SourceFile | ts.ModuleDeclaration> {
624634
return new Set(
625635
getExportedSymbolsUsingStatement(declaration)
@@ -672,6 +682,55 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
672682
}
673683

674684
function addImport(statement: ts.DeclarationStatement | ts.SourceFile): void {
685+
forEachImportOfStatement(statement, (imp: ImportOfStatement, referencedModuleInfo: ModuleInfo, importModuleSpecifier: string) => {
686+
// if a referenced module should be inlined we can just ignore it
687+
if (referencedModuleInfo.type !== ModuleType.ShouldBeImported) {
688+
return;
689+
}
690+
691+
const importItem = getImportItem(importModuleSpecifier);
692+
693+
if (ts.isImportEqualsDeclaration(imp)) {
694+
// import x = require("mod");
695+
addRequireImport(importItem, imp.name);
696+
return;
697+
}
698+
699+
if (ts.isExportSpecifier(imp)) {
700+
// export { El1, El2 as ExportedName } from 'module';
701+
addNamedImport(importItem, imp.name, imp.propertyName || imp.name);
702+
return;
703+
}
704+
705+
if (ts.isNamespaceExport(imp)) {
706+
// export * as name from 'module';
707+
addNsImport(importItem, imp.name);
708+
return;
709+
}
710+
711+
if (ts.isImportClause(imp) && imp.name !== undefined) {
712+
// import name from 'module';
713+
addDefaultImport(importItem, imp.name);
714+
return;
715+
}
716+
717+
if (ts.isImportSpecifier(imp)) {
718+
// import { El1, El2 as ImportedName } from 'module';
719+
addNamedImport(importItem, imp.name, imp.propertyName || imp.name);
720+
return;
721+
}
722+
723+
if (ts.isNamespaceImport(imp)) {
724+
// import * as name from 'module';
725+
addNsImport(importItem, imp.name);
726+
return;
727+
}
728+
});
729+
}
730+
731+
type ImportOfStatement = ts.ImportEqualsDeclaration | ts.ExportSpecifier | ts.NamespaceExport | ts.ImportClause | ts.ImportSpecifier | ts.NamespaceImport;
732+
733+
function forEachImportOfStatement(statement: ts.DeclarationStatement | ts.SourceFile, callback: (imp: ImportOfStatement, referencedModuleInfo: ModuleInfo, importModuleSpecifier: string) => void): void {
675734
if (!ts.isSourceFile(statement) && statement.name === undefined) {
676735
throw new Error(`Import/usage unnamed declaration: ${statement.getText()}`);
677736
}
@@ -702,15 +761,13 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
702761

703762
const referencedModuleInfo = getReferencedModuleInfo(st, criteria, typeChecker);
704763
// if a referenced module should be inlined we can just ignore it
705-
if (referencedModuleInfo === null || referencedModuleInfo.type !== ModuleType.ShouldBeImported) {
764+
if (referencedModuleInfo === null) {
706765
return;
707766
}
708767

709-
const importItem = getImportItem(importModuleSpecifier);
710-
711768
if (ts.isImportEqualsDeclaration(st)) {
712769
if (areDeclarationSame(statement, st)) {
713-
addRequireImport(importItem, st.name);
770+
callback(st, referencedModuleInfo, importModuleSpecifier);
714771
}
715772

716773
return;
@@ -722,18 +779,18 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
722779
st.exportClause.elements
723780
.filter(areDeclarationSame.bind(null, statement))
724781
.forEach((specifier: ts.ExportSpecifier) => {
725-
addNamedImport(importItem, specifier.name, specifier.propertyName || specifier.name);
782+
callback(specifier, referencedModuleInfo, importModuleSpecifier);
726783
});
727784
} else {
728785
// export * as name from 'module';
729786
if (isNodeUsed(st.exportClause)) {
730-
addNsImport(importItem, st.exportClause.name);
787+
callback(st.exportClause, referencedModuleInfo, importModuleSpecifier);
731788
}
732789
}
733790
} else if (ts.isImportDeclaration(st) && st.importClause !== undefined) {
734791
if (st.importClause.name !== undefined && areDeclarationSame(statement, st.importClause)) {
735792
// import name from 'module';
736-
addDefaultImport(importItem, st.importClause.name);
793+
callback(st.importClause, referencedModuleInfo, importModuleSpecifier);
737794
}
738795

739796
if (st.importClause.namedBindings !== undefined) {
@@ -742,12 +799,12 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
742799
st.importClause.namedBindings.elements
743800
.filter(areDeclarationSame.bind(null, statement))
744801
.forEach((specifier: ts.ImportSpecifier) => {
745-
addNamedImport(importItem, specifier.name, specifier.propertyName || specifier.name);
802+
callback(specifier, referencedModuleInfo, importModuleSpecifier);
746803
});
747804
} else {
748805
// import * as name from 'module';
749806
if (isNodeUsed(st.importClause)) {
750-
addNsImport(importItem, st.importClause.namedBindings.name);
807+
callback(st.importClause.namedBindings, referencedModuleInfo, importModuleSpecifier);
751808
}
752809
}
753810
}
@@ -1000,11 +1057,12 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
10001057
addSymbolToNamespaceExports(namespaceExports, symbol);
10011058
}
10021059

1060+
// eslint-disable-next-line complexity
10031061
function getIdentifierOfNamespaceImportFromInlinedModule(nsSymbol: ts.Symbol): ts.Identifier | null {
10041062
// handling namespaced re-exports/imports
10051063
// e.g. `export * as NS from './local-module';` or `import * as NS from './local-module'; export { NS }`
10061064
for (const decl of getDeclarationsForSymbol(nsSymbol)) {
1007-
if (!ts.isNamespaceExport(decl) && !ts.isExportSpecifier(decl)) {
1065+
if (!ts.isNamespaceExport(decl) && !ts.isExportSpecifier(decl) && !ts.isNamespaceImport(decl)) {
10081066
continue;
10091067
}
10101068

@@ -1013,6 +1071,11 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
10131071
return decl.name;
10141072
}
10151073

1074+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
1075+
if (ts.isNamespaceImport(decl) && !isReferencedModuleImportable(decl.parent.parent as ts.ImportDeclaration)) {
1076+
return decl.name;
1077+
}
1078+
10161079
if (ts.isExportSpecifier(decl)) {
10171080
// if it is export specifier then it should exporting a local symbol i.e. without a module specifier (e.g. `export { NS };` or `export { NS as NewNsName };`)
10181081
if (decl.parent.parent.moduleSpecifier !== undefined) {
@@ -1128,8 +1191,37 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
11281191
updateFn(sourceFile.statements, currentModule);
11291192

11301193
// handle `import * as module` usage if it's used as whole module
1131-
if (currentModule.type === ModuleType.ShouldBeImported && isNodeUsed(sourceFile)) {
1132-
updateImportsForStatement(sourceFile);
1194+
if (isNodeUsed(sourceFile)) {
1195+
switch (currentModule.type) {
1196+
case ModuleType.ShouldBeImported: {
1197+
updateImportsForStatement(sourceFile);
1198+
break;
1199+
}
1200+
1201+
case ModuleType.ShouldBeInlined: {
1202+
const sourceFileSymbol = getNodeSymbol(sourceFile, typeChecker);
1203+
if (sourceFileSymbol === null || sourceFileSymbol.exports === undefined) {
1204+
throw new Error(`Cannot find symbol or exports for source file ${sourceFile.fileName}`);
1205+
}
1206+
1207+
let namespaceIdentifier: ts.Identifier | null = null;
1208+
1209+
forEachImportOfStatement(sourceFile, (imp: ImportOfStatement) => {
1210+
// here we want to handle creation of artificial namespace for a inlined module
1211+
// so we don't care about other type of imports/exports - only these that create a "namespace"
1212+
if (ts.isNamespaceExport(imp) || ts.isNamespaceImport(imp)) {
1213+
namespaceIdentifier = imp.name;
1214+
}
1215+
});
1216+
1217+
if (namespaceIdentifier === null) {
1218+
break;
1219+
}
1220+
1221+
createNamespaceForExports(sourceFileSymbol.exports, getNodeOwnSymbol(namespaceIdentifier, typeChecker));
1222+
break;
1223+
}
1224+
}
11331225
}
11341226
}
11351227

@@ -1161,10 +1253,10 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
11611253
{
11621254
...collectionResult,
11631255
resolveIdentifierName: (identifier: ts.Identifier | ts.QualifiedName | ts.PropertyAccessEntityNameExpression): string | null => {
1164-
if (ts.isIdentifier(identifier)) {
1165-
return collisionsResolver.resolveReferencedIdentifier(identifier);
1166-
} else {
1256+
if (ts.isPropertyAccessOrQualifiedName(identifier)) {
11671257
return collisionsResolver.resolveReferencedQualifiedName(identifier);
1258+
} else {
1259+
return collisionsResolver.resolveReferencedIdentifier(identifier);
11681260
}
11691261
},
11701262
// eslint-disable-next-line complexity

src/types-usage-evaluator.ts

+50-7
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class TypesUsageEvaluator {
5050

5151
visitedSymbols.add(symbol);
5252
if (this.isSymbolUsedBySymbolImpl(symbol, toSymbol, visitedSymbols)) {
53-
return true;
53+
return this.setUsageCacheValue(fromSymbol, toSymbol, true);
5454
}
5555
}
5656
}
@@ -109,6 +109,14 @@ export class TypesUsageEvaluator {
109109
this.addUsagesForNamespacedModule(node.exportClause, node.moduleSpecifier as ts.StringLiteral);
110110
}
111111

112+
// `import * as ns from 'mod'`
113+
if (ts.isImportDeclaration(node) && node.moduleSpecifier !== undefined && node.importClause?.namedBindings !== undefined && ts.isNamespaceImport(node.importClause.namedBindings)) {
114+
// for namespaced imports we don't want to include module's exports into usage
115+
// because only exports actually "assign" all exports to a namespace node
116+
// namespaced imports affect only local scope (unless it is exported, but it handled elsewhere)
117+
this.addUsagesForNamespacedModule(node.importClause.namedBindings, node.moduleSpecifier as ts.StringLiteral, false);
118+
}
119+
112120
// `export {}` or `export {} from 'mod'`
113121
if (ts.isExportDeclaration(node) && node.exportClause !== undefined && ts.isNamedExports(node.exportClause)) {
114122
for (const exportElement of node.exportClause.elements) {
@@ -162,7 +170,7 @@ export class TypesUsageEvaluator {
162170
}
163171
}
164172

165-
private addUsagesForNamespacedModule(namespaceNode: ts.NamespaceImport | ts.NamespaceExport, moduleSpecifier: ts.StringLiteral): void {
173+
private addUsagesForNamespacedModule(namespaceNode: ts.NamespaceImport | ts.NamespaceExport, moduleSpecifier: ts.StringLiteral, includeExports: boolean = true): void {
166174
// note that we shouldn't resolve the actual symbol for the namespace
167175
// as in some circumstances it will be resolved to the source file
168176
// i.e. namespaceSymbol would become referencedModuleSymbol so it would be no-op
@@ -175,8 +183,10 @@ export class TypesUsageEvaluator {
175183
const resolvedNamespaceSymbol = this.getSymbol(namespaceNode.name);
176184
this.addUsages(resolvedNamespaceSymbol, namespaceSymbol);
177185

178-
// if a referenced source file has any exports, they should be added "to the usage" as they all are re-exported/imported
179-
this.addExportsToSymbol(referencedSourceFileSymbol.exports, referencedSourceFileSymbol);
186+
if (includeExports) {
187+
// if a referenced source file has any exports, they should be added "to the usage" as they all are re-exported/imported
188+
this.addExportsToSymbol(referencedSourceFileSymbol.exports, referencedSourceFileSymbol);
189+
}
180190
}
181191

182192
private addExportsToSymbol(exports: ts.SymbolTable | undefined, parentSymbol: ts.Symbol, visitedSymbols: Set<ts.Symbol> = new Set()): void {
@@ -206,12 +216,29 @@ export class TypesUsageEvaluator {
206216
}
207217

208218
private computeUsagesRecursively(parent: ts.Node, parentSymbol: ts.Symbol): void {
209-
ts.forEachChild(parent, (child: ts.Node) => {
219+
const processUsageForChild = (child: ts.Node) => {
210220
if (child.kind === ts.SyntaxKind.JSDoc) {
211221
return;
212222
}
213223

214-
this.computeUsagesRecursively(child, parentSymbol);
224+
let recursionStartNode = child;
225+
if (ts.isQualifiedName(child) && !ts.isQualifiedName(child.parent)) {
226+
const leftmostSymbol = this.getNodeOwnSymbol(child.left);
227+
228+
// i.e. `import * as NS from './local-module'`
229+
const namespaceImport = getDeclarationsForSymbol(leftmostSymbol).find(ts.isNamespaceImport);
230+
if (namespaceImport !== undefined) {
231+
// if a node is a qualified name and its top-level part was created by a namespaced import
232+
// then we shouldn't add usages of that "namespaced import" to the parent symbol
233+
// because we can just import the referenced symbol directly, without wrapping with a namespace
234+
recursionStartNode = child.right;
235+
236+
// recursive processing doesn't process a node itself so we need to handle it separately
237+
processUsageForChild(recursionStartNode);
238+
}
239+
}
240+
241+
this.computeUsagesRecursively(recursionStartNode, parentSymbol);
215242

216243
if (ts.isIdentifier(child) || child.kind === ts.SyntaxKind.DefaultKeyword) {
217244
// identifiers in labelled tuples don't have symbols for their labels
@@ -226,8 +253,24 @@ export class TypesUsageEvaluator {
226253
}
227254

228255
this.addUsages(this.getSymbol(child), parentSymbol);
256+
257+
if (!ts.isQualifiedName(child.parent)) {
258+
const childOwnSymbol = this.getNodeOwnSymbol(child);
259+
260+
// i.e. `import * as NS from './local-module'`
261+
const namespaceImport = getDeclarationsForSymbol(childOwnSymbol).find(ts.isNamespaceImport);
262+
if (namespaceImport !== undefined) {
263+
// if a node is an identifier and not part of a qualified name
264+
// and it was created as part of namespaced import
265+
// then we need to assign all exports of referenced module into that namespace
266+
// because they might not be added previously while processing imports/exports
267+
this.addUsagesForNamespacedModule(namespaceImport, namespaceImport.parent.parent.moduleSpecifier as ts.StringLiteral, true);
268+
}
269+
}
229270
}
230-
});
271+
};
272+
273+
ts.forEachChild(parent, processUsageForChild);
231274
}
232275

233276
private addUsages(childSymbol: ts.Symbol, parentSymbol: ts.Symbol): void {

tests/e2e/test-cases/export-wrapped-with-namespace/output.d.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ interface MyNamespace2 {
1919
}
2020
export type Type = MyInt;
2121

22+
declare namespace MyNamespace4 {
23+
export { Interface, MyInt$2 as MyInt, MyString$2 as MyString, MyType, MyType2, func$2 as func, subNs };
24+
}
25+
declare namespace subNs {
26+
export { MyType2 };
27+
}
2228
declare namespace MyNamespace {
2329
export { Interface, MyInt, MyString, func };
2430
}
@@ -28,15 +34,9 @@ declare namespace MyNamespace1 {
2834
declare namespace MyNamespace2 {
2935
export { Interface, MyInt$1 as MyInt, MyString$1 as MyString, func$1 as func };
3036
}
31-
declare namespace subNs {
32-
export { MyType2 };
33-
}
3437
declare namespace MyNamespace3 {
3538
export { Interface, MyInt$2 as MyInt, MyString$2 as MyString, MyType, MyType2, func$2 as func, subNs };
3639
}
37-
declare namespace MyNamespace4 {
38-
export { Interface, MyInt$2 as MyInt, MyString$2 as MyString, MyType, MyType2, func$2 as func, subNs };
39-
}
4040

4141
export {
4242
MyNamespace,

0 commit comments

Comments
 (0)