Skip to content

Commit

Permalink
Fixed handling import * statements with local usage but not directl…
Browse files Browse the repository at this point in the history
…y exporting

Fixes #304
  • Loading branch information
timocov committed Apr 1, 2024
1 parent 34a18af commit eb862bf
Show file tree
Hide file tree
Showing 13 changed files with 217 additions and 49 deletions.
156 changes: 124 additions & 32 deletions src/bundle-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:

const typesUsageEvaluator = new TypesUsageEvaluator(sourceFiles, typeChecker);

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

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

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

case ModuleType.ShouldBeImported:
Expand Down Expand Up @@ -590,7 +594,7 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
}
}

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

for (const statementToImport of statementsToImport) {
if (shouldNodeBeImported(statementToImport as ts.DeclarationStatement)) {
addImport(statementToImport as ts.DeclarationStatement);

// if we're going to add import of any statement in the bundle
// we should check whether the library of that statement
// could be referenced via triple-slash reference-types directive
// because the project which will use bundled declaration file
// can have `types: []` in the tsconfig and it'll fail
// this is especially related to the types packages
// which declares different modules in their declarations
// e.g. @types/node has declaration for "packages" events, fs, path and so on
const sourceFile = statementToImport.getSourceFile();
const moduleInfo = getFileModuleInfo(sourceFile.fileName, criteria);
if (moduleInfo.type === ModuleType.ShouldBeReferencedAsTypes) {
addTypesReference(moduleInfo.typesLibraryName);
}
callback(statementToImport as ts.DeclarationStatement);
}
}
}

function updateImportsForStatement(statement: ts.Statement | ts.SourceFile | ts.ExportSpecifier): void {
forEachNodeThatShouldBeImported(statement, (statementToImport: ts.DeclarationStatement) => {
addImport(statementToImport);

// if we're going to add import of any statement in the bundle
// we should check whether the library of that statement
// could be referenced via triple-slash reference-types directive
// because the project which will use bundled declaration file
// can have `types: []` in the tsconfig and it'll fail
// this is especially related to the types packages
// which declares different modules in their declarations
// e.g. @types/node has declaration for "packages" events, fs, path and so on
const sourceFile = statementToImport.getSourceFile();
const moduleInfo = getFileModuleInfo(sourceFile.fileName, criteria);
if (moduleInfo.type === ModuleType.ShouldBeReferencedAsTypes) {
addTypesReference(moduleInfo.typesLibraryName);
}
});
}

function getDeclarationUsagesSourceFiles(declaration: ts.NamedDeclaration): Set<ts.SourceFile | ts.ModuleDeclaration> {
return new Set(
getExportedSymbolsUsingStatement(declaration)
Expand Down Expand Up @@ -672,6 +682,55 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
}

function addImport(statement: ts.DeclarationStatement | ts.SourceFile): void {
forEachImportOfStatement(statement, (imp: ImportOfStatement, referencedModuleInfo: ModuleInfo, importModuleSpecifier: string) => {
// if a referenced module should be inlined we can just ignore it
if (referencedModuleInfo.type !== ModuleType.ShouldBeImported) {
return;
}

const importItem = getImportItem(importModuleSpecifier);

if (ts.isImportEqualsDeclaration(imp)) {
// import x = require("mod");
addRequireImport(importItem, imp.name);
return;
}

if (ts.isExportSpecifier(imp)) {
// export { El1, El2 as ExportedName } from 'module';
addNamedImport(importItem, imp.name, imp.propertyName || imp.name);
return;
}

if (ts.isNamespaceExport(imp)) {
// export * as name from 'module';
addNsImport(importItem, imp.name);
return;
}

if (ts.isImportClause(imp) && imp.name !== undefined) {
// import name from 'module';
addDefaultImport(importItem, imp.name);
return;
}

if (ts.isImportSpecifier(imp)) {
// import { El1, El2 as ImportedName } from 'module';
addNamedImport(importItem, imp.name, imp.propertyName || imp.name);
return;
}

if (ts.isNamespaceImport(imp)) {
// import * as name from 'module';
addNsImport(importItem, imp.name);
return;
}
});
}

type ImportOfStatement = ts.ImportEqualsDeclaration | ts.ExportSpecifier | ts.NamespaceExport | ts.ImportClause | ts.ImportSpecifier | ts.NamespaceImport;

function forEachImportOfStatement(statement: ts.DeclarationStatement | ts.SourceFile, callback: (imp: ImportOfStatement, referencedModuleInfo: ModuleInfo, importModuleSpecifier: string) => void): void {
if (!ts.isSourceFile(statement) && statement.name === undefined) {
throw new Error(`Import/usage unnamed declaration: ${statement.getText()}`);
}
Expand Down Expand Up @@ -702,15 +761,13 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:

const referencedModuleInfo = getReferencedModuleInfo(st, criteria, typeChecker);
// if a referenced module should be inlined we can just ignore it
if (referencedModuleInfo === null || referencedModuleInfo.type !== ModuleType.ShouldBeImported) {
if (referencedModuleInfo === null) {
return;
}

const importItem = getImportItem(importModuleSpecifier);

if (ts.isImportEqualsDeclaration(st)) {
if (areDeclarationSame(statement, st)) {
addRequireImport(importItem, st.name);
callback(st, referencedModuleInfo, importModuleSpecifier);
}

return;
Expand All @@ -722,18 +779,18 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
st.exportClause.elements
.filter(areDeclarationSame.bind(null, statement))
.forEach((specifier: ts.ExportSpecifier) => {
addNamedImport(importItem, specifier.name, specifier.propertyName || specifier.name);
callback(specifier, referencedModuleInfo, importModuleSpecifier);
});
} else {
// export * as name from 'module';
if (isNodeUsed(st.exportClause)) {
addNsImport(importItem, st.exportClause.name);
callback(st.exportClause, referencedModuleInfo, importModuleSpecifier);
}
}
} else if (ts.isImportDeclaration(st) && st.importClause !== undefined) {
if (st.importClause.name !== undefined && areDeclarationSame(statement, st.importClause)) {
// import name from 'module';
addDefaultImport(importItem, st.importClause.name);
callback(st.importClause, referencedModuleInfo, importModuleSpecifier);
}

if (st.importClause.namedBindings !== undefined) {
Expand All @@ -742,12 +799,12 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
st.importClause.namedBindings.elements
.filter(areDeclarationSame.bind(null, statement))
.forEach((specifier: ts.ImportSpecifier) => {
addNamedImport(importItem, specifier.name, specifier.propertyName || specifier.name);
callback(specifier, referencedModuleInfo, importModuleSpecifier);
});
} else {
// import * as name from 'module';
if (isNodeUsed(st.importClause)) {
addNsImport(importItem, st.importClause.namedBindings.name);
callback(st.importClause.namedBindings, referencedModuleInfo, importModuleSpecifier);
}
}
}
Expand Down Expand Up @@ -1000,11 +1057,12 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
addSymbolToNamespaceExports(namespaceExports, symbol);
}

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

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

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
if (ts.isNamespaceImport(decl) && !isReferencedModuleImportable(decl.parent.parent as ts.ImportDeclaration)) {
return decl.name;
}

if (ts.isExportSpecifier(decl)) {
// 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 };`)
if (decl.parent.parent.moduleSpecifier !== undefined) {
Expand Down Expand Up @@ -1128,8 +1191,37 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
updateFn(sourceFile.statements, currentModule);

// handle `import * as module` usage if it's used as whole module
if (currentModule.type === ModuleType.ShouldBeImported && isNodeUsed(sourceFile)) {
updateImportsForStatement(sourceFile);
if (isNodeUsed(sourceFile)) {
switch (currentModule.type) {
case ModuleType.ShouldBeImported: {
updateImportsForStatement(sourceFile);
break;
}

case ModuleType.ShouldBeInlined: {
const sourceFileSymbol = getNodeSymbol(sourceFile, typeChecker);
if (sourceFileSymbol === null || sourceFileSymbol.exports === undefined) {
throw new Error(`Cannot find symbol or exports for source file ${sourceFile.fileName}`);
}

let namespaceIdentifier: ts.Identifier | null = null;

forEachImportOfStatement(sourceFile, (imp: ImportOfStatement) => {
// here we want to handle creation of artificial namespace for a inlined module
// so we don't care about other type of imports/exports - only these that create a "namespace"
if (ts.isNamespaceExport(imp) || ts.isNamespaceImport(imp)) {
namespaceIdentifier = imp.name;
}
});

if (namespaceIdentifier === null) {
break;
}

createNamespaceForExports(sourceFileSymbol.exports, getNodeOwnSymbol(namespaceIdentifier, typeChecker));
break;
}
}
}
}

Expand Down Expand Up @@ -1161,10 +1253,10 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
{
...collectionResult,
resolveIdentifierName: (identifier: ts.Identifier | ts.QualifiedName | ts.PropertyAccessEntityNameExpression): string | null => {
if (ts.isIdentifier(identifier)) {
return collisionsResolver.resolveReferencedIdentifier(identifier);
} else {
if (ts.isPropertyAccessOrQualifiedName(identifier)) {
return collisionsResolver.resolveReferencedQualifiedName(identifier);
} else {
return collisionsResolver.resolveReferencedIdentifier(identifier);
}
},
// eslint-disable-next-line complexity
Expand Down
57 changes: 50 additions & 7 deletions src/types-usage-evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class TypesUsageEvaluator {

visitedSymbols.add(symbol);
if (this.isSymbolUsedBySymbolImpl(symbol, toSymbol, visitedSymbols)) {
return true;
return this.setUsageCacheValue(fromSymbol, toSymbol, true);
}
}
}
Expand Down Expand Up @@ -109,6 +109,14 @@ export class TypesUsageEvaluator {
this.addUsagesForNamespacedModule(node.exportClause, node.moduleSpecifier as ts.StringLiteral);
}

// `import * as ns from 'mod'`
if (ts.isImportDeclaration(node) && node.moduleSpecifier !== undefined && node.importClause?.namedBindings !== undefined && ts.isNamespaceImport(node.importClause.namedBindings)) {
// for namespaced imports we don't want to include module's exports into usage
// because only exports actually "assign" all exports to a namespace node
// namespaced imports affect only local scope (unless it is exported, but it handled elsewhere)
this.addUsagesForNamespacedModule(node.importClause.namedBindings, node.moduleSpecifier as ts.StringLiteral, false);
}

// `export {}` or `export {} from 'mod'`
if (ts.isExportDeclaration(node) && node.exportClause !== undefined && ts.isNamedExports(node.exportClause)) {
for (const exportElement of node.exportClause.elements) {
Expand Down Expand Up @@ -162,7 +170,7 @@ export class TypesUsageEvaluator {
}
}

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

// if a referenced source file has any exports, they should be added "to the usage" as they all are re-exported/imported
this.addExportsToSymbol(referencedSourceFileSymbol.exports, referencedSourceFileSymbol);
if (includeExports) {
// if a referenced source file has any exports, they should be added "to the usage" as they all are re-exported/imported
this.addExportsToSymbol(referencedSourceFileSymbol.exports, referencedSourceFileSymbol);
}
}

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

private computeUsagesRecursively(parent: ts.Node, parentSymbol: ts.Symbol): void {
ts.forEachChild(parent, (child: ts.Node) => {
const processUsageForChild = (child: ts.Node) => {
if (child.kind === ts.SyntaxKind.JSDoc) {
return;
}

this.computeUsagesRecursively(child, parentSymbol);
let recursionStartNode = child;
if (ts.isQualifiedName(child) && !ts.isQualifiedName(child.parent)) {
const leftmostSymbol = this.getNodeOwnSymbol(child.left);

// i.e. `import * as NS from './local-module'`
const namespaceImport = getDeclarationsForSymbol(leftmostSymbol).find(ts.isNamespaceImport);
if (namespaceImport !== undefined) {
// if a node is a qualified name and its top-level part was created by a namespaced import
// then we shouldn't add usages of that "namespaced import" to the parent symbol
// because we can just import the referenced symbol directly, without wrapping with a namespace
recursionStartNode = child.right;

// recursive processing doesn't process a node itself so we need to handle it separately
processUsageForChild(recursionStartNode);
}
}

this.computeUsagesRecursively(recursionStartNode, parentSymbol);

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

this.addUsages(this.getSymbol(child), parentSymbol);

if (!ts.isQualifiedName(child.parent)) {
const childOwnSymbol = this.getNodeOwnSymbol(child);

// i.e. `import * as NS from './local-module'`
const namespaceImport = getDeclarationsForSymbol(childOwnSymbol).find(ts.isNamespaceImport);
if (namespaceImport !== undefined) {
// if a node is an identifier and not part of a qualified name
// and it was created as part of namespaced import
// then we need to assign all exports of referenced module into that namespace
// because they might not be added previously while processing imports/exports
this.addUsagesForNamespacedModule(namespaceImport, namespaceImport.parent.parent.moduleSpecifier as ts.StringLiteral, true);
}
}
}
});
};

ts.forEachChild(parent, processUsageForChild);
}

private addUsages(childSymbol: ts.Symbol, parentSymbol: ts.Symbol): void {
Expand Down
12 changes: 6 additions & 6 deletions tests/e2e/test-cases/export-wrapped-with-namespace/output.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ interface MyNamespace2 {
}
export type Type = MyInt;

declare namespace MyNamespace4 {
export { Interface, MyInt$2 as MyInt, MyString$2 as MyString, MyType, MyType2, func$2 as func, subNs };
}
declare namespace subNs {
export { MyType2 };
}
declare namespace MyNamespace {
export { Interface, MyInt, MyString, func };
}
Expand All @@ -28,15 +34,9 @@ declare namespace MyNamespace1 {
declare namespace MyNamespace2 {
export { Interface, MyInt$1 as MyInt, MyString$1 as MyString, func$1 as func };
}
declare namespace subNs {
export { MyType2 };
}
declare namespace MyNamespace3 {
export { Interface, MyInt$2 as MyInt, MyString$2 as MyString, MyType, MyType2, func$2 as func, subNs };
}
declare namespace MyNamespace4 {
export { Interface, MyInt$2 as MyInt, MyString$2 as MyString, MyType, MyType2, func$2 as func, subNs };
}

export {
MyNamespace,
Expand Down

0 comments on commit eb862bf

Please sign in to comment.