diff --git a/src/bundle-generator.ts b/src/bundle-generator.ts index 4b45589..26c6a02 100644 --- a/src/bundle-generator.ts +++ b/src/bundle-generator.ts @@ -4,16 +4,19 @@ import { compileDts } from './compile-dts'; import { TypesUsageEvaluator } from './types-usage-evaluator'; import { ExportType, - getActualSymbol, getClosestModuleLikeNode, getClosestSourceFileLikeNode, + getDeclarationsForExportedValues, getDeclarationsForSymbol, + getExportReferencedSymbol, getExportsForSourceFile, getExportsForStatement, getImportModuleName, getNodeName, + getNodeOwnSymbol, getNodeSymbol, getRootSourceFile, + getSymbolExportStarDeclaration, hasNodeModifier, isAmbientModule, isDeclareGlobalStatement, @@ -176,11 +179,8 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options: const rootFileExports = getExportsForSourceFile(typeChecker, rootSourceFileSymbol); const rootFileExportSymbols = rootFileExports.map((exp: SourceFileExport) => exp.symbol); - interface CollectingResult { - typesReferences: Set; - imports: Map; + interface CollectingResult extends Pick { statements: ts.Statement[]; - renamedExports: OutputParams['renamedExports']; } const collectionResult: CollectingResult = { @@ -188,6 +188,7 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options: imports: new Map(), statements: [], renamedExports: new Map(), + wrappedNamespaces: new Map(), }; const outputOptions: OutputOptions = entryConfig.output || {}; @@ -204,7 +205,12 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options: // if we have `export =` or `export * from` somewhere so we can decide that every declaration of exported symbol in this way // is "part of the exported module" and we need to update result according every member of each declaration // but treat they as current module (we do not need to update module info) - for (const declaration of getDeclarationsForExportedValues(exportAssignment)) { + for (const declaration of getDeclarationsForExportedValues(exportAssignment, typeChecker)) { + if (ts.isVariableDeclaration(declaration)) { + // variables will be processed separately anyway so no need to process them again here + continue; + } + let exportedDeclarations: readonly ts.Statement[] = []; if (ts.isExportDeclaration(exportAssignment) && ts.isSourceFile(declaration)) { @@ -286,7 +292,7 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options: } } - if (ts.isExportAssignment(statement) && statement.isExportEquals && currentModule.isExternal) { + if (ts.isExportAssignment(statement) && statement.isExportEquals && currentModule.type !== ModuleType.ShouldBeInlined) { updateResultForExternalExport(statement); continue; } @@ -331,19 +337,49 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options: updateResultImpl(statements); } - function updateResultForRootModule(statements: readonly ts.Statement[], currentModule: ModuleInfo): void { - // eslint-disable-next-line prefer-arrow/prefer-arrow-functions - function isReExportFromImportableModule(statement: ts.ExportDeclaration): boolean { - return getReferencedModuleInfo(statement, criteria, typeChecker)?.type === ModuleType.ShouldBeImported; + // eslint-disable-next-line prefer-arrow/prefer-arrow-functions + function isReferencedModuleImportable(statement: ts.ExportDeclaration | ts.ImportDeclaration): boolean { + return getReferencedModuleInfo(statement, criteria, typeChecker)?.type === ModuleType.ShouldBeImported; + } + + function handleExportDeclarationFromRootModule(exportDeclaration: ts.ExportDeclaration): void { + // `export * from 'importable-package'` + if (isReferencedModuleImportable(exportDeclaration) && exportDeclaration.exportClause === undefined) { + collectionResult.statements.push(exportDeclaration); + return; + } + + // `export { val, val2 }` + if (exportDeclaration.moduleSpecifier === undefined && exportDeclaration.exportClause !== undefined && ts.isNamedExports(exportDeclaration.exportClause)) { + for (const exportElement of exportDeclaration.exportClause.elements) { + const exportElementSymbol = getExportReferencedSymbol(exportElement, typeChecker); + + const namespaceImportFromImportableModule = getDeclarationsForSymbol(exportElementSymbol).find((importDecl: ts.Declaration): importDecl is ts.NamespaceImport => { + return ts.isNamespaceImport(importDecl) && isReferencedModuleImportable(importDecl.parent.parent); + }); + + if (namespaceImportFromImportableModule !== undefined) { + const importModuleSpecifier = getImportModuleName(namespaceImportFromImportableModule.parent.parent); + if (importModuleSpecifier === null) { + throw new Error(`Cannot get import module name from '${namespaceImportFromImportableModule.parent.parent.getText()}'`); + } + + addStartImport( + getImportItem(importModuleSpecifier), + namespaceImportFromImportableModule.name + ); + } + } } + } + function updateResultForRootModule(statements: readonly ts.Statement[], currentModule: ModuleInfo): void { updateResultForAnyModule(statements, currentModule); // add skipped by `updateResult` exports for (const statement of statements) { - // `export * from 'importable-package'` - if (ts.isExportDeclaration(statement) && isReExportFromImportableModule(statement) && statement.exportClause === undefined) { - collectionResult.statements.push(statement); + if (ts.isExportDeclaration(statement)) { + handleExportDeclarationFromRootModule(statement); continue; } @@ -428,6 +464,42 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options: ); } + function getImportItem(importModuleSpecifier: string): ModuleImportsSet { + let importItem = collectionResult.imports.get(importModuleSpecifier); + if (importItem === undefined) { + importItem = { + defaultImports: new Set(), + namedImports: new Map(), + starImport: null, + requireImports: new Set(), + }; + + collectionResult.imports.set(importModuleSpecifier, importItem); + } + + return importItem; + } + + function addRequireImport(importItem: ModuleImportsSet, preferredLocalName: ts.Identifier): void { + importItem.requireImports.add(collisionsResolver.addTopLevelIdentifier(preferredLocalName)); + } + + function addNamedImport(importItem: ModuleImportsSet, preferredLocalName: ts.Identifier, importedIdentifier: ts.Identifier) { + const newLocalName = collisionsResolver.addTopLevelIdentifier(preferredLocalName); + const importedName = importedIdentifier.text; + importItem.namedImports.set(newLocalName, importedName); + } + + function addStartImport(importItem: ModuleImportsSet, preferredLocalName: ts.Identifier) { + if (importItem.starImport === null) { + importItem.starImport = collisionsResolver.addTopLevelIdentifier(preferredLocalName); + } + } + + function addDefaultImport(importItem: ModuleImportsSet, preferredLocalName: ts.Identifier) { + importItem.defaultImports.add(collisionsResolver.addTopLevelIdentifier(preferredLocalName)); + } + function addImport(statement: ts.DeclarationStatement | ts.SourceFile): void { if (!ts.isSourceFile(statement) && statement.name === undefined) { throw new Error(`Import/usage unnamed declaration: ${statement.getText()}`); @@ -460,21 +532,11 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options: return; } - let importItem = collectionResult.imports.get(importModuleSpecifier); - if (importItem === undefined) { - importItem = { - defaultImports: new Set(), - namedImports: new Map(), - starImport: null, - requireImports: new Set(), - }; - - collectionResult.imports.set(importModuleSpecifier, importItem); - } + const importItem = getImportItem(importModuleSpecifier); if (ts.isImportEqualsDeclaration(st)) { if (areDeclarationSame(statement, st)) { - importItem.requireImports.add(collisionsResolver.addTopLevelIdentifier(st.name)); + addRequireImport(importItem, st.name); } return; @@ -489,15 +551,12 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options: exportClause.elements .filter(areDeclarationSame.bind(null, statement)) .forEach((specifier: ts.ExportSpecifier) => { - const newLocalName = collisionsResolver.addTopLevelIdentifier(specifier.name); - const importedName = (specifier.propertyName || specifier.name).text; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - importItem!.namedImports.set(newLocalName, importedName); + addNamedImport(importItem, specifier.name, specifier.propertyName || specifier.name); }); } else { // export * as name from 'module'; - if (importItem.starImport === null && isNodeUsed(exportClause)) { - importItem.starImport = collisionsResolver.addTopLevelIdentifier(exportClause.name); + if (isNodeUsed(exportClause)) { + addStartImport(importItem, exportClause.name); } } } else { @@ -505,7 +564,7 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options: const importClause = st.importClause!; if (importClause.name !== undefined && areDeclarationSame(statement, importClause)) { // import name from 'module'; - importItem.defaultImports.add(collisionsResolver.addTopLevelIdentifier(importClause.name)); + addDefaultImport(importItem, importClause.name); } if (importClause.namedBindings !== undefined) { @@ -514,15 +573,12 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options: importClause.namedBindings.elements .filter(areDeclarationSame.bind(null, statement)) .forEach((specifier: ts.ImportSpecifier) => { - const newLocalName = collisionsResolver.addTopLevelIdentifier(specifier.name); - const importedName = (specifier.propertyName || specifier.name).text; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - importItem!.namedImports.set(newLocalName, importedName); + addNamedImport(importItem, specifier.name, specifier.propertyName || specifier.name); }); } else { // import * as name from 'module'; - if (importItem.starImport === null && isNodeUsed(importClause)) { - importItem.starImport = collisionsResolver.addTopLevelIdentifier(importClause.namedBindings.name); + if (isNodeUsed(importClause)) { + addStartImport(importItem, importClause.namedBindings.name); } } } @@ -665,19 +721,113 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options: return false; } - function getDeclarationsForExportedValues(exp: ts.ExportAssignment | ts.ExportDeclaration): ts.Declaration[] { - const nodeForSymbol = ts.isExportAssignment(exp) ? exp.expression : exp.moduleSpecifier; - if (nodeForSymbol === undefined) { - return []; + function createNamespaceForExports(exports: ts.SymbolTable, namespaceSymbol: ts.Symbol): string | null { + function addSymbolToNamespaceExports(namespaceExports: Map, symbol: ts.Symbol): void { + const symbolKnownNames = collisionsResolver.namesForSymbol(symbol); + if (symbolKnownNames.size === 0) { + throw new Error(`Cannot get local names for symbol '${symbol.getName()}' while generating namespaced export`); + } + + namespaceExports.set(symbol.getName(), Array.from(symbolKnownNames)[0]); } - const symbolForExpression = typeChecker.getSymbolAtLocation(nodeForSymbol); - if (symbolForExpression === undefined) { - return []; + function processExportSymbol(namespaceExports: Map, symbol: ts.Symbol): void { + if (symbol.escapedName === ts.InternalSymbolName.ExportStar) { + // this means that an export contains `export * from 'module'` statement + const exportStarDeclaration = getSymbolExportStarDeclaration(symbol); + if (exportStarDeclaration.moduleSpecifier === undefined) { + throw new Error(`Export star declaration does not have a module specifier '${exportStarDeclaration.getText()}'`); + } + + if (isReferencedModuleImportable(exportStarDeclaration)) { + // in case of re-exporting from other modules directly we should import everything and re-export manually + // but it is not supported yet so lets just fail for now + throw new Error(`Having a re-export from an importable module as a part of namespaced export is not supported yet.`); + } + + const referencedSourceFileSymbol = getNodeOwnSymbol(exportStarDeclaration.moduleSpecifier, typeChecker); + referencedSourceFileSymbol.exports?.forEach( + processExportSymbol.bind(null, namespaceExports) + ); + + return; + } + + const namespaceExport = symbol.declarations?.find(ts.isNamespaceExport); + if (namespaceExport !== undefined && namespaceExport.parent.moduleSpecifier !== undefined) { + // `export * as ns from 'module'` + if (isReferencedModuleImportable(namespaceExport.parent)) { + // in case of an external export statement we should copy it as is + // here we assume that a namespace import will be added in other places + // so here we can just add re-export + addSymbolToNamespaceExports(namespaceExports, symbol); + return; + } + + const referencedSourceFileSymbol = getNodeOwnSymbol(namespaceExport.parent.moduleSpecifier, typeChecker); + + const localNamespaceName = referencedSourceFileSymbol.exports !== undefined + ? createNamespaceForExports(referencedSourceFileSymbol.exports, symbol) + : null + ; + + if (localNamespaceName !== null) { + namespaceExports.set(symbol.getName(), localNamespaceName); + } + + return; + } + + addSymbolToNamespaceExports(namespaceExports, symbol); } - const symbol = getActualSymbol(symbolForExpression, typeChecker); - return getDeclarationsForSymbol(symbol); + // 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(namespaceSymbol)) { + if (!ts.isNamespaceExport(decl) && !ts.isExportSpecifier(decl)) { + continue; + } + + // if it is namespace export then it should be from a inlined module (e.g. `export * as NS from './local-module';`) + if (ts.isNamespaceExport(decl) && isReferencedModuleImportable(decl.parent)) { + continue; + } + + 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) { + continue; + } + + const declarationSymbol = getExportReferencedSymbol(decl, typeChecker); + + // but also that local symbol should be a namespace imported from inlined module + // i.e. `import * as NS from './local-module'` + const isNamespaceImportFromInlinedModule = getDeclarationsForSymbol(declarationSymbol).some((importDecl: ts.Declaration) => { + return ts.isNamespaceImport(importDecl) && !isReferencedModuleImportable(importDecl.parent.parent); + }); + + if (!isNamespaceImportFromInlinedModule) { + continue; + } + } + + const namespaceExports = new Map(); + exports.forEach( + processExportSymbol.bind(null, namespaceExports) + ); + + if (namespaceExports.size !== 0) { + const namespaceLocalName = collisionsResolver.addTopLevelIdentifier(decl.name); + collectionResult.wrappedNamespaces.set(namespaceLocalName, namespaceExports); + return namespaceLocalName; + } + + // we just need to find one suitable declaration, no need to iterate over the rest of the declarations + break; + } + + return null; } function syncExports(): void { @@ -688,6 +838,18 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options: continue; } + // if resolved symbol is a "synthetic" symbol already (i.e. not namespace module) + // then we should create a namespace for its exports + // otherwise most likely it will be inlined as is anyway so we don't need to do anything + const namespaceLocalName = exp.symbol.flags & ts.SymbolFlags.ValueModule && exp.symbol.exports !== undefined + ? createNamespaceForExports(exp.symbol.exports, exp.originalSymbol) + : null + ; + + if (namespaceLocalName !== null) { + collectionResult.renamedExports.set(exp.exportedName, namespaceLocalName); + } + const symbolKnownNames = collisionsResolver.namesForSymbol(exp.symbol); if (symbolKnownNames.size === 0) { // that's fine if a symbol doesn't exists in collisions resolver because it operates on top-level symbols only diff --git a/src/collisions-resolver.ts b/src/collisions-resolver.ts index 40697a2..913efac 100644 --- a/src/collisions-resolver.ts +++ b/src/collisions-resolver.ts @@ -2,7 +2,9 @@ import * as ts from 'typescript'; import { getActualSymbol, + getClosestModuleLikeNode, getDeclarationNameSymbol, + getDeclarationsForSymbol, } from './helpers/typescript'; import { verboseLog } from './logger'; @@ -64,6 +66,8 @@ export class CollisionsResolver { * Resolves given {@link referencedIdentifier} to a name. * It assumes that a symbol for this identifier has been registered before by calling {@link addTopLevelIdentifier} method. * Otherwise it will return `null`. + * + * Note that a returned value might be of a different type of the identifier (e.g. {@link ts.QualifiedName} for a given {@link ts.Identifier}) */ public resolveReferencedIdentifier(referencedIdentifier: ts.Identifier): string | null { const identifierSymbol = getDeclarationNameSymbol(referencedIdentifier, this.typeChecker); @@ -73,22 +77,55 @@ export class CollisionsResolver { return null; } - const namesForSymbol = this.namesForSymbol(identifierSymbol); - const identifierText = referencedIdentifier.getText(); - if (namesForSymbol.has(identifierText)) { - // if the set of already registered names contains the one that is requested then lets use it - return identifierText; + // we assume that all symbols for a given identifier will be in the same scope (i.e. defined in the same namespaces-chain) + // so we can use any declaration to find that scope as they all will have the same scope + const symbolScopePath = this.getNodeScope(getDeclarationsForSymbol(identifierSymbol)[0]); + + // this scope defines where the current identifier is located + const currentIdentifierScope = this.getNodeScope(referencedIdentifier); + + if (symbolScopePath.length > 0 && currentIdentifierScope.length > 0 && symbolScopePath[0] === currentIdentifierScope[0]) { + // if a referenced symbol is declared in the same scope where it is located + // then just return its reference as is without any modification + // also note that in this method we're working with identifiers only (i.e. it cannot be a qualified name) + return referencedIdentifier.getText(); + } + + const topLevelIdentifierSymbol = symbolScopePath.length === 0 ? identifierSymbol : symbolScopePath[0]; + + const namesForTopLevelSymbol = this.namesForSymbol(topLevelIdentifierSymbol); + if (namesForTopLevelSymbol.size === 0) { + return null; } - const namesArray = Array.from(namesForSymbol); - for (const name of namesArray) { - // attempt to find a generated name first to provide identifiers close to the original code as much as possible - if (name.startsWith(`${identifierText}$`)) { - return name; + let topLevelName = symbolScopePath.length === 0 ? referencedIdentifier.getText() : topLevelIdentifierSymbol.getName(); + if (!namesForTopLevelSymbol.has(topLevelName)) { + // if the set of already registered names does not contain the one that is requested + + const topLevelNamesArray = Array.from(namesForTopLevelSymbol); + + // lets find more suitable name for a top level symbol + let suitableTopLevelName = topLevelNamesArray[0]; + for (const name of topLevelNamesArray) { + // attempt to find a generated name first to provide identifiers close to the original code as much as possible + if (name.startsWith(`${topLevelName}$`)) { + suitableTopLevelName = name; + break; + } } + + topLevelName = suitableTopLevelName; } - return namesArray[0] || null; + const newIdentifierParts = [ + ...symbolScopePath.map((symbol: ts.Symbol) => symbol.getName()), + referencedIdentifier.getText(), + ]; + + // we don't need to rename any symbol but top level only as only it can collide with other symbols + newIdentifierParts[0] = topLevelName; + + return newIdentifierParts.join('.'); } /** @@ -138,11 +175,35 @@ export class CollisionsResolver { const identifierParts = referencedIdentifier.getText().split('.'); // update top level part as it could get renamed above + // note that `topLevelName` might be a qualified name (e.g. with `.` in the name) + // but this is fine as we join with `.` below anyway + // but it is worth it to mention here ¯\_(ツ)_/¯ identifierParts[0] = topLevelName; return identifierParts.join('.'); } + /** + * Returns a node's scope where it is located in terms of namespaces/modules. + * E.g. A scope for `Opt` in `declare module foo { type Opt = number; }` is `[Symbol(foo)]` + */ + private getNodeScope(node: ts.Node): ts.Symbol[] { + const scopeIdentifiersPath: ts.Symbol[] = []; + + let currentNode: ts.Node = getClosestModuleLikeNode(node); + while (ts.isModuleDeclaration(currentNode) && ts.isIdentifier(currentNode.name)) { + const nameSymbol = getDeclarationNameSymbol(currentNode.name, this.typeChecker); + if (nameSymbol === null) { + throw new Error(`Cannot find symbol for identifier '${currentNode.name.getText()}'`); + } + + scopeIdentifiersPath.push(nameSymbol); + currentNode = getClosestModuleLikeNode(currentNode.parent); + } + + return scopeIdentifiersPath.reverse(); + } + private registerSymbol(identifierSymbol: ts.Symbol, preferredName: string): string | null { if (!renamingSupportedSymbols.some((flag: ts.SymbolFlags) => identifierSymbol.flags & flag)) { // if a symbol for something else that we don't support yet - skip diff --git a/src/generate-output.ts b/src/generate-output.ts index 3a203f7..1565f8c 100644 --- a/src/generate-output.ts +++ b/src/generate-output.ts @@ -15,6 +15,7 @@ export interface OutputParams extends OutputHelpers { imports: Map; statements: readonly ts.Statement[]; renamedExports: Map; + wrappedNamespaces: Map>; } export interface NeedStripDefaultKeywordResult { @@ -77,10 +78,25 @@ export function generateOutput(params: OutputParams, options: OutputOptions = {} resultOutput += `\n\n${statementsTextToString(statements)}`; } + if (params.wrappedNamespaces.size !== 0) { + resultOutput += `\n\n${ + Array.from(params.wrappedNamespaces.entries()) + .map(([namespaceName, exportedNames]: [string, Map]) => { + return `declare namespace ${namespaceName} {\n\texport { ${ + Array.from(exportedNames.entries()) + .map(([exportedName, localName]: [string, string]) => renamedExportValue(exportedName, localName)) + .sort() + .join(', ') + } };\n}`; + }) + .join('\n') + }`; + } + if (params.renamedExports.size !== 0) { resultOutput += `\n\nexport {\n\t${ Array.from(params.renamedExports.entries()) - .map(([exportedName, localName]: [string, string]) => exportedName !== localName ? `${localName} as ${exportedName}` : exportedName) + .map(([exportedName, localName]: [string, string]) => renamedExportValue(exportedName, localName)) .sort() .join(',\n\t') },\n};`; @@ -107,6 +123,14 @@ function statementsTextToString(statements: StatementText[]): string { return spacesToTabs(prettifyStatementsText(statementsText)); } +function renamedExportValue(exportedName: string, localName: string): string { + return exportedName !== localName ? `${localName} as ${exportedName}` : exportedName; +} + +function renamedImportValue(importedName: string, localName: string): string { + return importedName !== localName ? `${importedName} as ${localName}` : importedName; +} + function prettifyStatementsText(statementsText: string): string { const sourceFile = ts.createSourceFile('output.d.ts', statementsText, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS); const printer = ts.createPrinter( @@ -148,25 +172,18 @@ function getStatementText(statement: ts.Statement, includeSortingValue: boolean, } if (needResolveIdentifiers) { - if (ts.isPropertyAccessExpression(node) || ts.isQualifiedName(node)) { - const resolvedName = helpers.resolveIdentifierName(node as ts.PropertyAccessEntityNameExpression | ts.QualifiedName); + if (ts.isPropertyAccessExpression(node)) { + const resolvedName = helpers.resolveIdentifierName(node as ts.PropertyAccessEntityNameExpression); if (resolvedName !== null && resolvedName !== node.getText()) { const identifiers = resolvedName.split('.'); - let result: ts.PropertyAccessExpression | ts.QualifiedName | ts.Identifier = ts.factory.createIdentifier(identifiers[0]); + let result: ts.PropertyAccessExpression | ts.Identifier = ts.factory.createIdentifier(identifiers[0]); for (let index = 1; index < identifiers.length; index += 1) { - if (ts.isQualifiedName(node)) { - result = ts.factory.createQualifiedName( - result as ts.QualifiedName, - ts.factory.createIdentifier(identifiers[index]) - ); - } else { - result = ts.factory.createPropertyAccessExpression( - result as ts.PropertyAccessExpression, - ts.factory.createIdentifier(identifiers[index]) - ); - } + result = ts.factory.createPropertyAccessExpression( + result as ts.PropertyAccessExpression, + ts.factory.createIdentifier(identifiers[index]) + ); } return result; @@ -175,15 +192,26 @@ function getStatementText(statement: ts.Statement, includeSortingValue: boolean, return node; } - if (ts.isIdentifier(node)) { - // QualifiedName and PropertyAccessExpression are handled above already - if (ts.isQualifiedName(node.parent) || ts.isPropertyAccessExpression(node.parent)) { + if (ts.isIdentifier(node) || ts.isQualifiedName(node)) { + // QualifiedName and PropertyAccessExpression are handled separately + if (ts.isIdentifier(node) && (ts.isQualifiedName(node.parent) || ts.isPropertyAccessExpression(node.parent))) { return node; } const resolvedName = helpers.resolveIdentifierName(node); if (resolvedName !== null && resolvedName !== node.getText()) { - return ts.factory.createIdentifier(resolvedName); + const identifiers = resolvedName.split('.'); + + let result: ts.QualifiedName | ts.Identifier = ts.factory.createIdentifier(identifiers[0]); + + for (let index = 1; index < identifiers.length; index += 1) { + result = ts.factory.createQualifiedName( + result, + ts.factory.createIdentifier(identifiers[index]) + ); + } + + return result; } return node; @@ -285,7 +313,7 @@ function generateImports(libraryName: string, imports: ModuleImportsSet): string if (imports.namedImports.size !== 0) { result.push(`import { ${ Array.from(imports.namedImports.entries()) - .map(([localName, importedName]: [string, string]) => localName !== importedName ? `${importedName} as ${localName}` : importedName) + .map(([localName, importedName]: [string, string]) => renamedImportValue(importedName, localName)) .sort() .join(', ') } } ${fromEnding}`); diff --git a/src/helpers/typescript.ts b/src/helpers/typescript.ts index a5fa23c..a89956e 100644 --- a/src/helpers/typescript.ts +++ b/src/helpers/typescript.ts @@ -151,6 +151,7 @@ export const enum ExportType { export interface SourceFileExport { exportedName: string; symbol: ts.Symbol; + originalSymbol: ts.Symbol; type: ExportType; } @@ -162,6 +163,7 @@ export function getExportsForSourceFile(typeChecker: ts.TypeChecker, sourceFileS return [ { symbol, + originalSymbol: commonJsExport, type: ExportType.CommonJS, exportedName: '', }, @@ -171,7 +173,7 @@ export function getExportsForSourceFile(typeChecker: ts.TypeChecker, sourceFileS const result = typeChecker .getExportsOfModule(sourceFileSymbol) - .map((symbol: ts.Symbol) => ({ symbol, exportedName: symbol.name, type: ExportType.ES6Named })); + .map((symbol: ts.Symbol) => ({ symbol, originalSymbol: symbol, exportedName: symbol.name, type: ExportType.ES6Named })); if (sourceFileSymbol.exports !== undefined) { const defaultExportSymbol = sourceFileSymbol.exports.get(ts.InternalSymbolName.Default); @@ -184,6 +186,7 @@ export function getExportsForSourceFile(typeChecker: ts.TypeChecker, sourceFileS // but let's add it to be sure add if there is no such export result.push({ symbol: defaultExportSymbol, + originalSymbol: defaultExportSymbol, type: ExportType.ES6Default, exportedName: 'default', }); @@ -497,6 +500,15 @@ export function getRootSourceFile(program: ts.Program, rootFileName: string): ts return sourceFile; } +export function getNodeOwnSymbol(node: ts.Node, typeChecker: ts.TypeChecker): ts.Symbol { + const nodeSymbol = typeChecker.getSymbolAtLocation(node); + if (nodeSymbol === undefined) { + throw new Error(`Cannot find symbol for node "${node.getText()}" in "${node.parent.getText()}" from "${node.getSourceFile().fileName}"`); + } + + return nodeSymbol; +} + export function getNodeSymbol(node: ts.Node, typeChecker: ts.TypeChecker): ts.Symbol | null { if (ts.isSourceFile(node)) { const fileSymbol = typeChecker.getSymbolAtLocation(node); @@ -604,3 +616,46 @@ export function getImportModuleName(imp: ts.ImportEqualsDeclaration | ts.ImportD return null; } + +/** + * Returns a symbol that an {@link exportElement} reference to. + * + * For example, for given `export { Value }` it returns a declaration of `Value` whatever it is (import statement, interface declaration, etc). + */ +export function getExportReferencedSymbol(exportElement: ts.ExportSpecifier, typeChecker: ts.TypeChecker): ts.Symbol { + return exportElement.propertyName !== undefined + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ? typeChecker.getSymbolAtLocation(exportElement.propertyName)! + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + : typeChecker.getImmediateAliasedSymbol(typeChecker.getSymbolAtLocation(exportElement.name)!)! + ; +} + +export function getSymbolExportStarDeclaration(symbol: ts.Symbol): ts.ExportDeclaration { + if (symbol.escapedName !== ts.InternalSymbolName.ExportStar) { + throw new Error(`Only ExportStar symbol can have export star declaration, but got ${symbol.escapedName}`); + } + + // this means that an export contains `export * from 'module'` statement + const exportStarDeclaration = getDeclarationsForSymbol(symbol).find(ts.isExportDeclaration); + if (exportStarDeclaration === undefined || exportStarDeclaration.moduleSpecifier === undefined) { + throw new Error(`Cannot find export declaration for ${symbol.getName()} symbol`); + } + + return exportStarDeclaration; +} + +export function getDeclarationsForExportedValues(exp: ts.ExportAssignment | ts.ExportDeclaration, typeChecker: ts.TypeChecker): ts.Declaration[] { + const nodeForSymbol = ts.isExportAssignment(exp) ? exp.expression : exp.moduleSpecifier; + if (nodeForSymbol === undefined) { + return []; + } + + const symbolForExpression = typeChecker.getSymbolAtLocation(nodeForSymbol); + if (symbolForExpression === undefined) { + return []; + } + + const symbol = getActualSymbol(symbolForExpression, typeChecker); + return getDeclarationsForSymbol(symbol); +} diff --git a/src/types-usage-evaluator.ts b/src/types-usage-evaluator.ts index b6f358b..6063d88 100644 --- a/src/types-usage-evaluator.ts +++ b/src/types-usage-evaluator.ts @@ -1,7 +1,12 @@ import * as ts from 'typescript'; import { getActualSymbol, + getDeclarationsForExportedValues, + getDeclarationsForSymbol, + getExportReferencedSymbol, getNodeName, + getNodeOwnSymbol, + getSymbolExportStarDeclaration, isDeclareModule, isNodeNamedDeclaration, splitTransientSymbol, @@ -87,17 +92,53 @@ 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 !== undefined && node.importClause.namedBindings !== undefined && ts.isNamespaceImport(node.importClause.namedBindings)) { - this.addUsagesForNamespacedModule(node.importClause.namedBindings, node.moduleSpecifier as ts.StringLiteral); - } - // `export {}` or `export {} from 'mod'` if (ts.isExportDeclaration(node) && node.exportClause !== undefined && ts.isNamedExports(node.exportClause)) { for (const exportElement of node.exportClause.elements) { - const parentSymbol = this.getNodeOwnSymbol(exportElement.name); - const childSymbol = this.getSymbol(exportElement.propertyName || exportElement.name); - this.addUsages(childSymbol, parentSymbol); + const exportElementSymbol = getExportReferencedSymbol(exportElement, this.typeChecker); + + // i.e. `import * as NS from './local-module'` + const namespaceImportForElement = getDeclarationsForSymbol(exportElementSymbol).find(ts.isNamespaceImport); + if (namespaceImportForElement !== undefined) { + // the namespaced import itself doesn't add a "usage", but re-export of that imported namespace does + // so here we're handling the case where previously imported namespace import has been re-exported from a module + this.addUsagesForNamespacedModule(namespaceImportForElement, namespaceImportForElement.parent.parent.moduleSpecifier as ts.StringLiteral); + } + + // "link" referenced symbol with its import + this.addUsages(exportElementSymbol, this.getNodeOwnSymbol(exportElement.name)); + } + } + + // `export =` + if (ts.isExportAssignment(node) && node.isExportEquals) { + this.addUsagesForExportAssignment(node); + } + } + + private addUsagesForExportAssignment(exportAssignment: ts.ExportAssignment): void { + for (const declaration of getDeclarationsForExportedValues(exportAssignment, this.typeChecker)) { + // `declare module foobar {}` or `namespace foobar {}` + if (ts.isModuleDeclaration(declaration) && ts.isIdentifier(declaration.name) && declaration.body !== undefined && ts.isModuleBlock(declaration.body)) { + const moduleSymbol = this.getSymbol(declaration.name); + + for (const statement of declaration.body.statements) { + if (isNodeNamedDeclaration(statement) && statement.name !== undefined) { + const statementSymbol = this.getSymbol(statement.name); + if (statementSymbol !== null) { + // this feels counter-intuitive that we assign a statement as a parent of a module + // but this is what happens when you have `export=` statements + // you can import an interface declared in `export=` exported namespace + // via named import statement + // e.g. lets say you have `namespace foo { export interface Interface {} }; export = foo;` + // then you can import it like `import { Interface } from 'module'` + // in this case only `Interface` is used, but it is part of module `foo` + // which means that `foo` is used via using `Interface` + // if you're reading this - please stop using `export=` exports asap! + this.addUsages(moduleSymbol, statementSymbol); + } + } + } } } } @@ -110,6 +151,32 @@ export class TypesUsageEvaluator { const namespaceSymbol = this.getNodeOwnSymbol(namespaceNode.name); const referencedSourceFileSymbol = this.getSymbol(moduleSpecifier); this.addUsages(referencedSourceFileSymbol, namespaceSymbol); + + // but in case it is not resolved to the source file we need to link them + 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, namespaceSymbol); + this.addExportsToSymbol(referencedSourceFileSymbol.exports, referencedSourceFileSymbol); + } + + private addExportsToSymbol(exports: ts.SymbolTable | undefined, parentSymbol: ts.Symbol): void { + exports?.forEach((moduleExportedSymbol: ts.Symbol, name: ts.__String) => { + if (name === ts.InternalSymbolName.ExportStar) { + // this means that an export contains `export * from 'module'` statement + const exportStarDeclaration = getSymbolExportStarDeclaration(moduleExportedSymbol); + if (exportStarDeclaration.moduleSpecifier === undefined) { + throw new Error(`Export star declaration does not have a module specifier '${exportStarDeclaration.getText()}'`); + } + + const referencedSourceFileSymbol = this.getSymbol(exportStarDeclaration.moduleSpecifier); + this.addExportsToSymbol(referencedSourceFileSymbol.exports, parentSymbol); + return; + } + + this.addUsages(moduleExportedSymbol, parentSymbol); + }); } private computeUsagesRecursively(parent: ts.Node, parentSymbol: ts.Symbol): void { @@ -160,12 +227,7 @@ export class TypesUsageEvaluator { } private getNodeOwnSymbol(node: ts.Node): ts.Symbol { - const nodeSymbol = this.typeChecker.getSymbolAtLocation(node); - if (nodeSymbol === undefined) { - throw new Error(`Cannot find symbol for node "${node.getText()}" in "${node.parent.getText()}" from "${node.getSourceFile().fileName}"`); - } - - return nodeSymbol; + return getNodeOwnSymbol(node, this.typeChecker); } private getActualSymbol(symbol: ts.Symbol): ts.Symbol { diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-chain-inline/config.ts b/tests/e2e/test-cases/export-wrapped-with-namespace-chain-inline/config.ts new file mode 100644 index 0000000..5dfbc95 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-chain-inline/config.ts @@ -0,0 +1,12 @@ +import { TestCaseConfig } from '../test-case-config'; + +const config: TestCaseConfig = { + libraries: { + inlinedLibraries: ['package-with-export-eq'], + }, + output: { + exportReferencedTypes: false, + }, +}; + +export = config; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-chain-inline/first.ts b/tests/e2e/test-cases/export-wrapped-with-namespace-chain-inline/first.ts new file mode 100644 index 0000000..9fc5686 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-chain-inline/first.ts @@ -0,0 +1 @@ +export * as FirstNamespaceName from './second'; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-chain-inline/index.spec.js b/tests/e2e/test-cases/export-wrapped-with-namespace-chain-inline/index.spec.js new file mode 100644 index 0000000..c015c26 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-chain-inline/index.spec.js @@ -0,0 +1 @@ +require('../run-test-case').runTestCase(__dirname); diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-chain-inline/input.ts b/tests/e2e/test-cases/export-wrapped-with-namespace-chain-inline/input.ts new file mode 100644 index 0000000..87f6ac4 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-chain-inline/input.ts @@ -0,0 +1 @@ +export * as TopNamespaceName from './first'; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-chain-inline/output.d.ts b/tests/e2e/test-cases/export-wrapped-with-namespace-chain-inline/output.d.ts new file mode 100644 index 0000000..cfe5163 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-chain-inline/output.d.ts @@ -0,0 +1,22 @@ +import * as Ns from 'fake-package'; + +declare namespace MyModule { + export interface SomeCoolInterface { + field: string; + field2: number; + } +} +type A = string; + +declare namespace FirstNamespaceName { + export { A, MyModule as Ns1, Ns }; +} +declare namespace TopNamespaceName { + export { FirstNamespaceName }; +} + +export { + TopNamespaceName, +}; + +export {}; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-chain-inline/second.ts b/tests/e2e/test-cases/export-wrapped-with-namespace-chain-inline/second.ts new file mode 100644 index 0000000..66d0118 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-chain-inline/second.ts @@ -0,0 +1,7 @@ +import * as Ns1 from 'package-with-export-eq'; + +export type A = string; + +export * as Ns from 'fake-package'; + +export { Ns1 }; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-chain/config.ts b/tests/e2e/test-cases/export-wrapped-with-namespace-chain/config.ts new file mode 100644 index 0000000..d830495 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-chain/config.ts @@ -0,0 +1,9 @@ +import { TestCaseConfig } from '../test-case-config'; + +const config: TestCaseConfig = { + output: { + exportReferencedTypes: false, + }, +}; + +export = config; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-chain/first.ts b/tests/e2e/test-cases/export-wrapped-with-namespace-chain/first.ts new file mode 100644 index 0000000..9fc5686 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-chain/first.ts @@ -0,0 +1 @@ +export * as FirstNamespaceName from './second'; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-chain/index.spec.js b/tests/e2e/test-cases/export-wrapped-with-namespace-chain/index.spec.js new file mode 100644 index 0000000..c015c26 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-chain/index.spec.js @@ -0,0 +1 @@ +require('../run-test-case').runTestCase(__dirname); diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-chain/input.ts b/tests/e2e/test-cases/export-wrapped-with-namespace-chain/input.ts new file mode 100644 index 0000000..87f6ac4 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-chain/input.ts @@ -0,0 +1 @@ +export * as TopNamespaceName from './first'; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-chain/output.d.ts b/tests/e2e/test-cases/export-wrapped-with-namespace-chain/output.d.ts new file mode 100644 index 0000000..85d009c --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-chain/output.d.ts @@ -0,0 +1,20 @@ +import * as Ns from 'fake-package'; +import * as Ns1 from 'package-with-export-eq'; + +type A = string; + +declare namespace SecondNamespaceName { + export { A, Ns, Ns1 }; +} +declare namespace FirstNamespaceName { + export { SecondNamespaceName }; +} +declare namespace TopNamespaceName { + export { FirstNamespaceName }; +} + +export { + TopNamespaceName, +}; + +export {}; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-chain/second.ts b/tests/e2e/test-cases/export-wrapped-with-namespace-chain/second.ts new file mode 100644 index 0000000..4ed5583 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-chain/second.ts @@ -0,0 +1 @@ +export * as SecondNamespaceName from './third'; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-chain/third.ts b/tests/e2e/test-cases/export-wrapped-with-namespace-chain/third.ts new file mode 100644 index 0000000..5bc3dc0 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-chain/third.ts @@ -0,0 +1,10 @@ +import * as Ns1 from 'package-with-export-eq'; + +export type A = string; + +export * as Ns from 'fake-package'; + +export { Ns1 }; + +// this is not supported yet, uncomment once it is possible +// export * from 'fake-package'; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-export/config.ts b/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-export/config.ts new file mode 100644 index 0000000..d830495 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-export/config.ts @@ -0,0 +1,9 @@ +import { TestCaseConfig } from '../test-case-config'; + +const config: TestCaseConfig = { + output: { + exportReferencedTypes: false, + }, +}; + +export = config; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-export/index.spec.js b/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-export/index.spec.js new file mode 100644 index 0000000..c015c26 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-export/index.spec.js @@ -0,0 +1 @@ +require('../run-test-case').runTestCase(__dirname); diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-export/input.ts b/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-export/input.ts new file mode 100644 index 0000000..6a19bd8 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-export/input.ts @@ -0,0 +1,4 @@ +import * as newName from 'package-with-export-eq'; +import * as myLib from 'package-with-export-eq-variable'; + +export { newName, myLib }; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-export/output.d.ts b/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-export/output.d.ts new file mode 100644 index 0000000..86cb1e2 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-export/output.d.ts @@ -0,0 +1,9 @@ +import * as newName from 'package-with-export-eq'; +import * as myLib from 'package-with-export-eq-variable'; + +export { + myLib, + newName, +}; + +export {}; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-inline/config.ts b/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-inline/config.ts new file mode 100644 index 0000000..c79b5a5 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-inline/config.ts @@ -0,0 +1,12 @@ +import { TestCaseConfig } from '../test-case-config'; + +const config: TestCaseConfig = { + libraries: { + inlinedLibraries: ['package-with-export-eq', 'package-with-export-eq-variable'], + }, + output: { + exportReferencedTypes: false, + }, +}; + +export = config; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-inline/index.spec.js b/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-inline/index.spec.js new file mode 100644 index 0000000..c015c26 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-inline/index.spec.js @@ -0,0 +1 @@ +require('../run-test-case').runTestCase(__dirname); diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-inline/input.ts b/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-inline/input.ts new file mode 100644 index 0000000..6a19bd8 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-inline/input.ts @@ -0,0 +1,4 @@ +import * as newName from 'package-with-export-eq'; +import * as myLib from 'package-with-export-eq-variable'; + +export { newName, myLib }; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-inline/output.d.ts b/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-inline/output.d.ts new file mode 100644 index 0000000..9b73938 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace-export-eq-inline/output.d.ts @@ -0,0 +1,16 @@ +declare namespace MyModule { + export interface SomeCoolInterface { + field: string; + field2: number; + } +} +interface ExportedInterface { + field: number; +} +export const myLib: ExportedInterface; + +export { + MyModule as newName, +}; + +export {}; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace/another-exports.ts b/tests/e2e/test-cases/export-wrapped-with-namespace/another-exports.ts new file mode 100644 index 0000000..ce328af --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace/another-exports.ts @@ -0,0 +1,5 @@ +export type MyString = string; +export interface MyInt {} +export function func() {} + +export { Interface } from 'fake-package'; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace/config.ts b/tests/e2e/test-cases/export-wrapped-with-namespace/config.ts new file mode 100644 index 0000000..d830495 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace/config.ts @@ -0,0 +1,9 @@ +import { TestCaseConfig } from '../test-case-config'; + +const config: TestCaseConfig = { + output: { + exportReferencedTypes: false, + }, +}; + +export = config; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace/exports.ts b/tests/e2e/test-cases/export-wrapped-with-namespace/exports.ts new file mode 100644 index 0000000..ce328af --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace/exports.ts @@ -0,0 +1,5 @@ +export type MyString = string; +export interface MyInt {} +export function func() {} + +export { Interface } from 'fake-package'; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace/index.spec.js b/tests/e2e/test-cases/export-wrapped-with-namespace/index.spec.js new file mode 100644 index 0000000..c015c26 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace/index.spec.js @@ -0,0 +1 @@ +require('../run-test-case').runTestCase(__dirname); diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace/input.ts b/tests/e2e/test-cases/export-wrapped-with-namespace/input.ts new file mode 100644 index 0000000..c2cd5fa --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace/input.ts @@ -0,0 +1,18 @@ +export * as MyNamespace from './exports'; +export * as MyNamespace1 from './another-exports'; +export * as MyNamespace2 from './another-exports'; + +import * as SomeLocalNsName from './one-more-exports'; +import * as MyNamespace4 from './one-more-exports'; +import { MyInt } from './exports'; + +export interface MyNamespace2 { + field: MyInt; +} + +export type Type = MyInt; + +export { + SomeLocalNsName as MyNamespace3, + MyNamespace4, +}; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace/one-more-exports.ts b/tests/e2e/test-cases/export-wrapped-with-namespace/one-more-exports.ts new file mode 100644 index 0000000..87718c2 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace/one-more-exports.ts @@ -0,0 +1,8 @@ +export type MyString = string; +export interface MyInt {} +export function func() {} + +export { Interface } from 'fake-package'; + +export * from './type'; +export * as subNs from './type2'; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace/output.d.ts b/tests/e2e/test-cases/export-wrapped-with-namespace/output.d.ts new file mode 100644 index 0000000..b07d44f --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace/output.d.ts @@ -0,0 +1,49 @@ +import { Interface } from 'fake-package'; + +type MyString = string; +interface MyInt { +} +declare function func(): void; +type MyString$1 = string; +interface MyInt$1 { +} +declare function func$1(): void; +type MyType = string; +type MyType2 = string; +type MyString$2 = string; +interface MyInt$2 { +} +declare function func$2(): void; +interface MyNamespace2 { + field: MyInt; +} +export type Type = MyInt; + +declare namespace MyNamespace { + export { Interface, MyInt, MyString, func }; +} +declare namespace MyNamespace1 { + export { Interface, MyInt$1 as MyInt, MyString$1 as MyString, func$1 as func }; +} +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, func$2 as func, subNs }; +} +declare namespace MyNamespace4 { + export { Interface, MyInt$2 as MyInt, MyString$2 as MyString, MyType, func$2 as func, subNs }; +} + +export { + MyNamespace, + MyNamespace1, + MyNamespace2, + MyNamespace3, + MyNamespace4, +}; + +export {}; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace/type.ts b/tests/e2e/test-cases/export-wrapped-with-namespace/type.ts new file mode 100644 index 0000000..ed7545c --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace/type.ts @@ -0,0 +1 @@ +export type MyType = string; diff --git a/tests/e2e/test-cases/export-wrapped-with-namespace/type2.ts b/tests/e2e/test-cases/export-wrapped-with-namespace/type2.ts new file mode 100644 index 0000000..25a5011 --- /dev/null +++ b/tests/e2e/test-cases/export-wrapped-with-namespace/type2.ts @@ -0,0 +1 @@ +export type MyType2 = string; diff --git a/tests/e2e/test-cases/import-from-namespace-in-cjs/output.d.ts b/tests/e2e/test-cases/import-from-namespace-in-cjs/output.d.ts index 9fea859..74db482 100644 --- a/tests/e2e/test-cases/import-from-namespace-in-cjs/output.d.ts +++ b/tests/e2e/test-cases/import-from-namespace-in-cjs/output.d.ts @@ -1,6 +1,11 @@ -export interface Options { +declare namespace ora { + interface Options { + } } -export declare const ExportedValue: Options; -export type ExportedType = Options; +declare const ora: { + (options: ora.Options): any; +}; +export declare const ExportedValue: ora.Options; +export type ExportedType = ora.Options; export {}; diff --git a/tests/e2e/test-cases/names-collision-across-files/export-eq.ts b/tests/e2e/test-cases/names-collision-across-files/export-eq.ts new file mode 100644 index 0000000..5c247fc --- /dev/null +++ b/tests/e2e/test-cases/names-collision-across-files/export-eq.ts @@ -0,0 +1,27 @@ +namespace ExportEqNs { + export namespace InternalNs { + export type NewType = string; + } + + export type Bar = ExportEqNs.Foo; + export type Foo = String; + + export namespace InternalNs2 { + export type Type21 = InternalNs3.Type31; + export type Type22 = InternalNs2.InternalNs3.Type31; + export type Type23 = ExportEqNs.InternalNs2.InternalNs3.Type31; + + export namespace InternalNs3 { + export type Type31 = InternalNs4.Type; + export type Type32 = InternalNs3.InternalNs4.Type; + export type Type33 = InternalNs2.InternalNs3.InternalNs4.Type; + export type Type34 = ExportEqNs.InternalNs2.InternalNs3.InternalNs4.Type; + + export namespace InternalNs4 { + export type Type = string; + } + } + } +} + +export = ExportEqNs; diff --git a/tests/e2e/test-cases/names-collision-across-files/input.ts b/tests/e2e/test-cases/names-collision-across-files/input.ts index 57a3e11..2617af8 100644 --- a/tests/e2e/test-cases/names-collision-across-files/input.ts +++ b/tests/e2e/test-cases/names-collision-across-files/input.ts @@ -1,3 +1,8 @@ +import { ExportEqNs } from './ns'; +import { InternalNs } from './export-eq'; + +export type ExportedNsType = InternalNs.NewType; + export { default as TEMPLATE1, MergedSymbol as MS1, @@ -28,3 +33,5 @@ export { export { Inter } from './import-star-1'; export { Inter2 } from './import-star-2'; export { MyType } from './type'; + +export { ExportEqNs }; diff --git a/tests/e2e/test-cases/names-collision-across-files/ns.ts b/tests/e2e/test-cases/names-collision-across-files/ns.ts new file mode 100644 index 0000000..01bce94 --- /dev/null +++ b/tests/e2e/test-cases/names-collision-across-files/ns.ts @@ -0,0 +1 @@ +export type ExportEqNs = string; diff --git a/tests/e2e/test-cases/names-collision-across-files/output.d.ts b/tests/e2e/test-cases/names-collision-across-files/output.d.ts index 42d88ec..0820517 100644 --- a/tests/e2e/test-cases/names-collision-across-files/output.d.ts +++ b/tests/e2e/test-cases/names-collision-across-files/output.d.ts @@ -1,6 +1,28 @@ import * as fakePackage from 'fake-package'; import { Interface as FPI1, Interface as FPI2, Interface as Interface$2 } from 'fake-package'; +export type ExportEqNs = string; +declare namespace ExportEqNs$1 { + namespace InternalNs { + type NewType = string; + } + type Bar = ExportEqNs$1.Foo; + type Foo = String; + namespace InternalNs2 { + type Type21 = InternalNs3.Type31; + type Type22 = InternalNs2.InternalNs3.Type31; + type Type23 = ExportEqNs$1.InternalNs2.InternalNs3.Type31; + namespace InternalNs3 { + type Type31 = InternalNs4.Type; + type Type32 = InternalNs3.InternalNs4.Type; + type Type33 = InternalNs2.InternalNs3.InternalNs4.Type; + type Type34 = ExportEqNs$1.InternalNs2.InternalNs3.InternalNs4.Type; + namespace InternalNs4 { + type Type = string; + } + } + } +} declare const TEMPLATE = "template1"; declare const MergedSymbol = ""; interface MergedSymbol { @@ -56,6 +78,7 @@ export interface Inter2 { field7: Interface$2; } export type MyType = Interface$2; +export type ExportedNsType = ExportEqNs$1.InternalNs.NewType; export { AnotherInterface as AI1, diff --git a/tests/e2e/test-cases/node_modules/@types/package-with-export-eq-variable/index.d.ts b/tests/e2e/test-cases/node_modules/@types/package-with-export-eq-variable/index.d.ts new file mode 100644 index 0000000..1e2cdf7 --- /dev/null +++ b/tests/e2e/test-cases/node_modules/@types/package-with-export-eq-variable/index.d.ts @@ -0,0 +1,8 @@ +interface ExportedInterface { + field: number; +} + +declare module 'package-with-export-eq-variable' { + const myLib: ExportedInterface; + export = myLib; +} diff --git a/tests/e2e/test-cases/run-test-case.ts b/tests/e2e/test-cases/run-test-case.ts index 2e424d9..baa4c48 100644 --- a/tests/e2e/test-cases/run-test-case.ts +++ b/tests/e2e/test-cases/run-test-case.ts @@ -2,8 +2,6 @@ import * as fs from 'fs'; import * as path from 'path'; import * as assert from 'assert'; -import * as ts from 'typescript'; - import { generateDtsBundle } from '../../../src/bundle-generator'; import { TestCaseConfig } from './test-case-config';