From ff1dcaefbfaa3bdd58e6b33f43820440a7167978 Mon Sep 17 00:00:00 2001 From: Evgeniy Timokhov Date: Sun, 19 Nov 2023 22:39:09 +0000 Subject: [PATCH] Added support for name-collision resolution Fixes #116 Fixes #130 Fixed #184 --- .mocharc.js | 6 + README.md | 1 - src/bundle-generator.ts | 1200 +++++++---------- src/collisions-resolver.ts | 182 +++ src/generate-output.ts | 76 +- src/helpers/typescript.ts | 82 +- src/module-info.ts | 32 +- .../export-default-from-entry/output.d.ts | 6 +- .../export-default-from-non-entry/output.d.ts | 6 +- .../export-default-unnamed-statement/input.ts | 4 + .../number.ts | 1 + .../object.ts | 3 + .../output.d.ts | 32 +- .../string.ts | 1 + .../e2e/test-cases/import()-type/my-type.d.ts | 1 + .../test-cases/import()-type/namespace.d.ts | 3 + .../e2e/test-cases/import()-type/output.d.ts | 4 + .../test-cases/merged-namespaces/config.ts | 9 + .../e2e/test-cases/merged-namespaces/input.ts | 17 + tests/e2e/test-cases/merged-namespaces/ns1.ts | 33 + tests/e2e/test-cases/merged-namespaces/ns2.ts | 33 + .../test-cases/merged-namespaces/output.d.ts | 71 + .../names-collision-across-files/config.ts | 6 + .../names-collision-across-files/file1.ts | 25 + .../names-collision-across-files/file2.ts | 25 + .../import-star-1.ts | 17 + .../import-star-2.ts | 17 + .../names-collision-across-files/input.ts | 29 + .../names-collision-across-files/output.d.ts | 79 ++ .../output.d.ts | 6 +- .../rename-local-class/class-rename-1.ts | 3 + .../rename-local-class/class-rename-2.ts | 3 + .../test-cases/rename-local-class/class.ts | 1 + .../test-cases/rename-local-class/config.ts | 5 + .../test-cases/rename-local-class/input.ts | 2 + .../test-cases/rename-local-class/output.d.ts | 8 + 36 files changed, 1302 insertions(+), 727 deletions(-) create mode 100644 src/collisions-resolver.ts create mode 100644 tests/e2e/test-cases/export-default-unnamed-statement/number.ts create mode 100644 tests/e2e/test-cases/export-default-unnamed-statement/object.ts create mode 100644 tests/e2e/test-cases/export-default-unnamed-statement/string.ts create mode 100644 tests/e2e/test-cases/import()-type/namespace.d.ts create mode 100644 tests/e2e/test-cases/merged-namespaces/config.ts create mode 100644 tests/e2e/test-cases/merged-namespaces/input.ts create mode 100644 tests/e2e/test-cases/merged-namespaces/ns1.ts create mode 100644 tests/e2e/test-cases/merged-namespaces/ns2.ts create mode 100644 tests/e2e/test-cases/merged-namespaces/output.d.ts create mode 100644 tests/e2e/test-cases/names-collision-across-files/config.ts create mode 100644 tests/e2e/test-cases/names-collision-across-files/file1.ts create mode 100644 tests/e2e/test-cases/names-collision-across-files/file2.ts create mode 100644 tests/e2e/test-cases/names-collision-across-files/import-star-1.ts create mode 100644 tests/e2e/test-cases/names-collision-across-files/import-star-2.ts create mode 100644 tests/e2e/test-cases/names-collision-across-files/input.ts create mode 100644 tests/e2e/test-cases/names-collision-across-files/output.d.ts create mode 100644 tests/e2e/test-cases/rename-local-class/class-rename-1.ts create mode 100644 tests/e2e/test-cases/rename-local-class/class-rename-2.ts create mode 100644 tests/e2e/test-cases/rename-local-class/class.ts create mode 100644 tests/e2e/test-cases/rename-local-class/config.ts create mode 100644 tests/e2e/test-cases/rename-local-class/input.ts create mode 100644 tests/e2e/test-cases/rename-local-class/output.d.ts diff --git a/.mocharc.js b/.mocharc.js index b137924..604bc30 100644 --- a/.mocharc.js +++ b/.mocharc.js @@ -1,3 +1,9 @@ +const path = require('path'); + +// override tsconfig for tests +process.env.TS_NODE_PROJECT = path.resolve(__dirname, './tsconfig.options.json'); +process.env.TS_NODE_TRANSPILE_ONLY = 'true'; + const config = { require: [ 'ts-node/register', diff --git a/README.md b/README.md index 0269bce..24c320c 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,6 @@ but: ## Known limitations -1. All your types should have different names inside a bundle. If you have 2 `interface Options {}` they will be merged by `TypeScript` and you will get wrong definitions (see https://github.com/timocov/dts-bundle-generator/issues/116 and https://github.com/timocov/dts-bundle-generator/issues/130) 1. Importing and exporting with renaming in modules outside of entry points is limited/not supported as yet (see https://github.com/timocov/dts-bundle-generator/issues/184) [ci-img]: https://github.com/timocov/dts-bundle-generator/workflows/CI%20Test/badge.svg?branch=master diff --git a/src/bundle-generator.ts b/src/bundle-generator.ts index e7afda4..47f0d2a 100644 --- a/src/bundle-generator.ts +++ b/src/bundle-generator.ts @@ -1,14 +1,17 @@ import * as ts from 'typescript'; -import * as path from 'path'; import { compileDts } from './compile-dts'; import { TypesUsageEvaluator } from './types-usage-evaluator'; import { + ExportType, getActualSymbol, getClosestModuleLikeNode, + getClosestSourceFileLikeNode, getDeclarationsForSymbol, getExportsForSourceFile, getExportsForStatement, + getImportModuleName, + getNodeName, getNodeSymbol, getRootSourceFile, hasNodeModifier, @@ -16,27 +19,26 @@ import { isDeclareGlobalStatement, isDeclareModule, isNodeNamedDeclaration, - resolveIdentifier, SourceFileExport, splitTransientSymbol, } from './helpers/typescript'; -import { fixPath } from './helpers/fix-path'; - import { - getModuleInfo, + getFileModuleInfo, + getModuleLikeModuleInfo, + getReferencedModuleInfo, ModuleCriteria, ModuleInfo, ModuleType, } from './module-info'; -import { generateOutput, ModuleImportsSet } from './generate-output'; +import { generateOutput, ModuleImportsSet, OutputParams } from './generate-output'; import { normalLog, verboseLog, - warnLog, } from './logger'; +import { CollisionsResolver } from './collisions-resolver'; export interface CompilationOptions { /** @@ -148,14 +150,12 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options: const typesUsageEvaluator = new TypesUsageEvaluator(sourceFiles, typeChecker); - let uniqueNameCounter = 1; - - return entries.map((entry: EntryPointConfig) => { - normalLog(`Processing ${entry.filePath}`); + return entries.map((entryConfig: EntryPointConfig) => { + normalLog(`Processing ${entryConfig.filePath}`); - const newRootFilePath = rootFilesRemapping.get(entry.filePath); + const newRootFilePath = rootFilesRemapping.get(entryConfig.filePath); if (newRootFilePath === undefined) { - throw new Error(`Cannot remap root source file ${entry.filePath}`); + throw new Error(`Cannot remap root source file ${entryConfig.filePath}`); } const rootSourceFile = getRootSourceFile(program, newRootFilePath); @@ -164,7 +164,7 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options: throw new Error(`Symbol for root source file ${newRootFilePath} not found`); } - const librariesOptions: LibrariesOptions = entry.libraries || {}; + const librariesOptions: LibrariesOptions = entryConfig.libraries || {}; const criteria: ModuleCriteria = { allowedTypesLibraries: librariesOptions.allowedTypesLibraries, @@ -176,805 +176,627 @@ 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; + statements: ts.Statement[]; + renamedExports: OutputParams['renamedExports']; + } + const collectionResult: CollectingResult = { typesReferences: new Set(), imports: new Map(), statements: [], renamedExports: [], - declarationsRenaming: new Map(), }; - const outputOptions: OutputOptions = entry.output || {}; + const outputOptions: OutputOptions = entryConfig.output || {}; const inlineDeclareGlobals = Boolean(outputOptions.inlineDeclareGlobals); - const needStripDefaultKeywordForStatement = (statement: ts.Statement | ts.NamedDeclaration) => { - const statementExports = getExportsForStatement(rootFileExports, typeChecker, statement); - // a statement should have a 'default' keyword only if it it declared in the root source file - // otherwise it will be re-exported via `export { name as default }` - const defaultExport = statementExports.find((exp: SourceFileExport) => exp.exportedName === 'default'); + const collisionsResolver = new CollisionsResolver(typeChecker); + + function updateResultForAnyModule(statements: readonly ts.Statement[], currentModule: ModuleInfo): void { + // contains a set of modules that were visited already + // can be used to prevent infinite recursion in updating results in re-exports + const visitedModules = new Set(); + + function updateResultForExternalExport(exportAssignment: ts.ExportAssignment | ts.ExportDeclaration): void { + // 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)) { + let exportedDeclarations: readonly ts.Statement[] = []; + + if (ts.isModuleDeclaration(declaration)) { + if (declaration.body !== undefined && ts.isModuleBlock(declaration.body)) { + const referencedModule = getReferencedModuleInfo(declaration, criteria, typeChecker); + if (referencedModule !== null) { + if (visitedModules.has(referencedModule.fileName)) { + continue; + } + + visitedModules.add(referencedModule.fileName); + } - return { - needStrip: defaultExport === undefined || defaultExport.originalName !== 'default' && statement.getSourceFile() !== rootSourceFile, - newName: isNodeNamedDeclaration(statement) ? collectionResult.declarationsRenaming.get(statement) : undefined, - }; - }; + exportedDeclarations = declaration.body.statements; + } + } else { + exportedDeclarations = [declaration as unknown as ts.Statement]; + } - const updateResultCommonParams = { - isStatementUsed: (statement: ts.Statement | ts.SourceFile) => isNodeUsed( - statement, - rootFileExportSymbols, - typesUsageEvaluator, - typeChecker, - criteria, - inlineDeclareGlobals - ), - shouldStatementBeImported: (statement: ts.DeclarationStatement) => { - return shouldNodeBeImported( - statement, - rootFileExportSymbols, - typesUsageEvaluator, - typeChecker, - program.isSourceFileDefaultLibrary.bind(program), - criteria, - inlineDeclareGlobals - ); - }, - shouldDeclareGlobalBeInlined: (currentModule: ModuleInfo) => inlineDeclareGlobals && currentModule.type === ModuleType.ShouldBeInlined, - shouldDeclareExternalModuleBeInlined: () => Boolean(outputOptions.inlineDeclareExternals), - getModuleInfo: (fileNameOrModuleLike: string | ts.SourceFile | ts.ModuleDeclaration) => { - if (typeof fileNameOrModuleLike !== 'string') { - return getModuleLikeInfo(fileNameOrModuleLike, criteria); + updateResultImpl(exportedDeclarations); } + } - return getModuleInfo(fileNameOrModuleLike, criteria); - }, - resolveIdentifier: (identifier: ts.Identifier) => { - const resolvedDeclaration = resolveIdentifier(typeChecker, identifier); - if (resolvedDeclaration === undefined) { - return undefined; - } + // eslint-disable-next-line complexity + function updateResultImpl(statementsToProcess: readonly ts.Statement[]): void { + for (const statement of statementsToProcess) { + // we should skip import statements + if (statement.kind === ts.SyntaxKind.ImportDeclaration || statement.kind === ts.SyntaxKind.ImportEqualsDeclaration) { + continue; + } - const storedValue = collectionResult.declarationsRenaming.get(resolvedDeclaration); - if (storedValue !== undefined) { - return storedValue; - } + if (isDeclareModule(statement)) { + updateResultForModuleDeclaration(statement, currentModule); - let identifierName = resolvedDeclaration.name?.getText(); - if ( - hasNodeModifier(resolvedDeclaration, ts.SyntaxKind.DefaultKeyword) - && resolvedDeclaration.name === undefined - && needStripDefaultKeywordForStatement(resolvedDeclaration).needStrip - ) { - // this means that a node is default-exported from its module but from entry point it is exported with a different name(s) - // so we have to generate some random name and then re-export it with really exported names - identifierName = `__DTS_BUNDLE_GENERATOR__GENERATED_NAME$${uniqueNameCounter++}`; - collectionResult.declarationsRenaming.set(resolvedDeclaration, identifierName); - } + // if a statement is `declare module "module" {}` then don't process it below + // as it is handled already in `updateResultForModuleDeclaration` + // but if it is `declare module Module {}` then it can be used in types and imports + // so in this case it needs to be checked for "usages" below + if (ts.isStringLiteral(statement.name)) { + continue; + } + } - return identifierName; - }, - getDeclarationsForExportedValues: (exp: ts.ExportAssignment | ts.ExportDeclaration) => { - const nodeForSymbol = ts.isExportAssignment(exp) ? exp.expression : exp.moduleSpecifier; - if (nodeForSymbol === undefined) { - return []; - } + if (currentModule.type === ModuleType.ShouldBeUsedForModulesOnly) { + continue; + } - const symbolForExpression = typeChecker.getSymbolAtLocation(nodeForSymbol); - if (symbolForExpression === undefined) { - return []; - } + if (isDeclareGlobalStatement(statement) && inlineDeclareGlobals && currentModule.type === ModuleType.ShouldBeInlined) { + collectionResult.statements.push(statement); + continue; + } - const symbol = getActualSymbol(symbolForExpression, typeChecker); - return getDeclarationsForSymbol(symbol); - }, - getDeclarationUsagesSourceFiles: (declaration: ts.NamedDeclaration) => { - return getDeclarationUsagesSourceFiles( - declaration, - rootFileExportSymbols, - typesUsageEvaluator, - typeChecker, - criteria, - inlineDeclareGlobals - ); - }, - areDeclarationSame: (left: ts.NamedDeclaration, right: ts.NamedDeclaration) => { - const leftSymbols = splitTransientSymbol(getNodeSymbol(left, typeChecker) as ts.Symbol, typeChecker); - const rightSymbols = splitTransientSymbol(getNodeSymbol(right, typeChecker) as ts.Symbol, typeChecker); + if (ts.isExportDeclaration(statement)) { + if (!currentModule.isExternal) { + continue; + } - return leftSymbols.some((leftSymbol: ts.Symbol) => rightSymbols.includes(leftSymbol)); - }, - resolveReferencedModule: (node: NodeWithReferencedModule) => { - let moduleName: ts.Expression | ts.LiteralTypeNode | undefined; - - if (ts.isExportDeclaration(node) || ts.isImportDeclaration(node)) { - moduleName = node.moduleSpecifier; - } else if (ts.isModuleDeclaration(node)) { - moduleName = node.name; - } else if (ts.isImportEqualsDeclaration(node)) { - if (ts.isExternalModuleReference(node.moduleReference)) { - moduleName = node.moduleReference.expression; + // `export * from` + if (statement.exportClause === undefined) { + updateResultForExternalExport(statement); + continue; + } + + // `export { val }` + if (ts.isNamedExports(statement.exportClause) && currentModule.type === ModuleType.ShouldBeImported) { + updateImportsForStatement(statement); + continue; + } } - } else if (ts.isLiteralTypeNode(node.argument) && ts.isStringLiteral(node.argument.literal)) { - moduleName = node.argument.literal; - } - if (moduleName === undefined) { - return null; - } + if (ts.isExportAssignment(statement) && statement.isExportEquals && currentModule.isExternal) { + updateResultForExternalExport(statement); + continue; + } + + if (!isNodeUsed(statement)) { + continue; + } - const moduleSymbol = typeChecker.getSymbolAtLocation(moduleName); - if (moduleSymbol === undefined) { - return null; + switch (currentModule.type) { + case ModuleType.ShouldBeReferencedAsTypes: + addTypesReference(currentModule.typesLibraryName); + break; + + case ModuleType.ShouldBeImported: + updateImportsForStatement(statement); + break; + + case ModuleType.ShouldBeInlined: + if (ts.isVariableStatement(statement)) { + for (const variableDeclaration of statement.declarationList.declarations) { + if (ts.isIdentifier(variableDeclaration.name)) { + collisionsResolver.addTopLevelIdentifier(variableDeclaration.name); + } + + // it seems that the compiler doesn't produce anything else (e.g. binding elements) in declaration files + // but it is still possible to write such code manually + // this feels like quite rare case so no support for now + } + } else if (isNodeNamedDeclaration(statement)) { + const statementName = getNodeName(statement); + if (statementName !== undefined) { + collisionsResolver.addTopLevelIdentifier(statementName as ts.Identifier | ts.DefaultKeyword); + } + } + + collectionResult.statements.push(statement); + break; + } } + } + + updateResultImpl(statements); + } - const symbol = getActualSymbol(moduleSymbol, typeChecker); - if (symbol.valueDeclaration === undefined) { - return null; + function updateResultForRootModule(statements: readonly ts.Statement[], currentModule: ModuleInfo): void { + function isReExportFromImportableModule(statement: ts.Statement): boolean { + if (!ts.isExportDeclaration(statement)) { + return false; } - if (ts.isSourceFile(symbol.valueDeclaration) || ts.isModuleDeclaration(symbol.valueDeclaration)) { - return symbol.valueDeclaration; + const resolvedModuleInfo = getReferencedModuleInfo(statement, criteria, typeChecker); + if (resolvedModuleInfo === null) { + return false; } - return null; - }, - }; + return resolvedModuleInfo.type === ModuleType.ShouldBeImported; + } - for (const sourceFile of sourceFiles) { - verboseLog(`\n\n======= Preparing file: ${sourceFile.fileName} =======`); + updateResultForAnyModule(statements, currentModule); - const prevStatementsCount = collectionResult.statements.length; - const updateFn = sourceFile === rootSourceFile ? updateResultForRootSourceFile : updateResult; - const currentModule = getModuleInfo(sourceFile.fileName, criteria); - const params: UpdateParams = { - ...updateResultCommonParams, - currentModule, - }; + // add skipped by `updateResult` exports + for (const statement of statements) { + // "export =" or "export {} from 'importable-package'" + if (ts.isExportAssignment(statement) && statement.isExportEquals || isReExportFromImportableModule(statement)) { + collectionResult.statements.push(statement); + continue; + } - updateFn(sourceFile.statements, params, collectionResult); + // "export default" + if (ts.isExportAssignment(statement) && !statement.isExportEquals) { + if (!ts.isIdentifier(statement.expression)) { + // `export default 123`, `export default "str"` + collectionResult.statements.push(statement); + } - // handle `import * as module` usage if it's used as whole module - if (currentModule.type === ModuleType.ShouldBeImported && updateResultCommonParams.isStatementUsed(sourceFile)) { - updateImportsForStatement(sourceFile, params, collectionResult); + continue; + } } + } - if (collectionResult.statements.length === prevStatementsCount) { - verboseLog(`No output for file: ${sourceFile.fileName}`); + function updateResultForModuleDeclaration(moduleDecl: ts.ModuleDeclaration, currentModule: ModuleInfo): void { + if (moduleDecl.body === undefined || !ts.isModuleBlock(moduleDecl.body)) { + return; } - } - if (entry.failOnClass) { - const classes = collectionResult.statements.filter(ts.isClassDeclaration); - if (classes.length !== 0) { - const classesNames = classes.map((c: ts.ClassDeclaration) => c.name === undefined ? 'anonymous class' : c.name.text); - throw new Error(`${classes.length} class statement(s) are found in generated dts: ${classesNames.join(', ')}`); + const referencedModuleInfo = getReferencedModuleInfo(moduleDecl, criteria, typeChecker); + if (referencedModuleInfo === null) { + return; } - } - // by default this option should be enabled - const exportReferencedTypes = outputOptions.exportReferencedTypes !== false; + // if we have declaration of external module inside internal one + if (!currentModule.isExternal && referencedModuleInfo.isExternal) { + // if it's allowed - we need to just add it to result without any processing + if (outputOptions.inlineDeclareExternals) { + collectionResult.statements.push(moduleDecl); + } - return generateOutput( - { - ...collectionResult, - needStripDefaultKeywordForStatement, - shouldStatementHasExportKeyword: (statement: ts.Statement) => { - const statementExports = getExportsForStatement(rootFileExports, typeChecker, statement); + return; + } - // If true, then no direct export was found. That means that node might have - // an export keyword (like interface, type, etc) otherwise, if there are - // only re-exports with renaming (like export { foo as bar }) we don't need - // to put export keyword for this statement because we'll re-export it in the way - const hasStatementDefaultKeyword = hasNodeModifier(statement, ts.SyntaxKind.DefaultKeyword); - let result = statementExports.length === 0 || statementExports.find((exp: SourceFileExport) => { - // "directly" means "without renaming" or "without additional node/statement" - // for instance, `class A {} export default A;` - here `statement` is `class A {}` - // it's default exported by `export default A;`, but class' statement itself doesn't have `export` keyword - // so we shouldn't add this either - const shouldBeDefaultExportedDirectly = exp.exportedName === 'default' && hasStatementDefaultKeyword && statement.getSourceFile() === rootSourceFile; - return shouldBeDefaultExportedDirectly || exp.exportedName === exp.originalName; - }) !== undefined; + updateResultForAnyModule(moduleDecl.body.statements, referencedModuleInfo); + } - // "direct export" means export from the root source file - // e.g. classes/functions/etc must be exported from the root source file to have an "export" keyword - // by default interfaces/types are exported even if they aren't directly exported (e.g. when they are referenced by other types) - // but if `exportReferencedTypes` option is disabled we have to check direct export for them either - const onlyDirectlyExportedShouldBeExported = !exportReferencedTypes - || ts.isClassDeclaration(statement) - || (ts.isEnumDeclaration(statement) && !hasNodeModifier(statement, ts.SyntaxKind.ConstKeyword)) - || ts.isFunctionDeclaration(statement) - || ts.isVariableStatement(statement) - || ts.isModuleDeclaration(statement); + function addTypesReference(library: string): void { + if (!collectionResult.typesReferences.has(library)) { + normalLog(`Library "${library}" will be added via reference directive`); + collectionResult.typesReferences.add(library); + } + } - if (onlyDirectlyExportedShouldBeExported) { - // "valuable" statements must be re-exported from root source file - // to having export keyword in declaration file - result = result && statementExports.length !== 0; - } else if (isAmbientModule(statement) || ts.isExportDeclaration(statement)) { - result = false; + function updateImportsForStatement(statement: ts.Statement | ts.SourceFile | ts.ExportSpecifier): void { + const statementsToImport = ts.isVariableStatement(statement) + ? statement.declarationList.declarations + : ts.isExportDeclaration(statement) && statement.exportClause !== undefined + ? ts.isNamespaceExport(statement.exportClause) + ? [statement.exportClause] + : statement.exportClause.elements + : [statement]; + + 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); } + } + } + } - return result; - }, - needStripConstFromConstEnum: (constEnum: ts.EnumDeclaration) => { - if (!program.getCompilerOptions().preserveConstEnums || !outputOptions.respectPreserveConstEnum) { - return false; - } + // eslint-disable-next-line prefer-arrow/prefer-arrow-functions + function getDeclarationUsagesSourceFiles(declaration: ts.NamedDeclaration): Set { + return new Set( + getExportedSymbolsUsingStatement(declaration) + .map((symbol: ts.Symbol) => getDeclarationsForSymbol(symbol)) + .reduce((acc: ts.Declaration[], val: ts.Declaration[]) => acc.concat(val), []) + .map(getClosestModuleLikeNode) + ); + } - const enumSymbol = getNodeSymbol(constEnum, typeChecker); - if (enumSymbol === null) { - return false; - } + function addImport(statement: ts.DeclarationStatement | ts.SourceFile): void { + if (!ts.isSourceFile(statement) && statement.name === undefined) { + throw new Error(`Import/usage unnamed declaration: ${statement.getText()}`); + } - return rootFileExportSymbols.includes(enumSymbol); - }, - needStripImportFromImportTypeNode: (node: ts.ImportTypeNode) => { - if (node.qualifier === undefined) { - return false; - } + getDeclarationUsagesSourceFiles(statement).forEach((sourceFile: ts.SourceFile | ts.ModuleDeclaration) => { + const sourceFileStatements = ts.isSourceFile(sourceFile) + ? sourceFile.statements + : (sourceFile.body as ts.ModuleBlock).statements; - if (!ts.isLiteralTypeNode(node.argument) || !ts.isStringLiteral(node.argument.literal)) { - return false; + sourceFileStatements.forEach((st: ts.Statement) => { + if (!ts.isImportEqualsDeclaration(st) && !ts.isImportDeclaration(st)) { + return; } - const resolvedModule = updateResultCommonParams.resolveReferencedModule(node); - if (resolvedModule === null) { - return false; + const importModuleSpecifier = getImportModuleName(st); + if (importModuleSpecifier === null) { + return; } - return updateResultCommonParams.getModuleInfo(resolvedModule).type === ModuleType.ShouldBeInlined; - }, - }, - { - sortStatements: outputOptions.sortNodes, - umdModuleName: outputOptions.umdModuleName, - noBanner: outputOptions.noBanner, - } - ); - }); -} - -interface CollectingResult { - typesReferences: Set; - imports: Map; - statements: ts.Statement[]; - renamedExports: string[]; - declarationsRenaming: Map; -} + 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) { + return; + } -type NodeWithReferencedModule = ts.ExportDeclaration | ts.ModuleDeclaration | ts.ImportTypeNode | ts.ImportEqualsDeclaration | ts.ImportDeclaration; + let importItem = collectionResult.imports.get(importModuleSpecifier); + if (importItem === undefined) { + importItem = { + defaultImports: new Set(), + namedImports: new Set(), + starImports: new Set(), + requireImports: new Set(), + }; -interface UpdateParams { - currentModule: ModuleInfo; - isStatementUsed(statement: ts.Statement): boolean; - shouldStatementBeImported(statement: ts.DeclarationStatement): boolean; - shouldDeclareGlobalBeInlined(currentModule: ModuleInfo, statement: ts.ModuleDeclaration): boolean; - shouldDeclareExternalModuleBeInlined(): boolean; - getModuleInfo(fileName: string | ts.SourceFile | ts.ModuleDeclaration): ModuleInfo; - /** - * Returns original name which is referenced by passed identifier. - * Could be used to resolve "default" identifier in exports. - */ - resolveIdentifier(identifier: ts.NamedDeclaration['name']): string | undefined; - getDeclarationsForExportedValues(exp: ts.ExportAssignment | ts.ExportDeclaration): ts.Declaration[]; - getDeclarationUsagesSourceFiles(declaration: ts.NamedDeclaration): Set; - areDeclarationSame(a: ts.NamedDeclaration, b: ts.NamedDeclaration): boolean; - resolveReferencedModule(node: NodeWithReferencedModule): ts.SourceFile | ts.ModuleDeclaration | null; -} + collectionResult.imports.set(importModuleSpecifier, importItem); + } -const skippedNodes = [ - ts.SyntaxKind.ImportDeclaration, - ts.SyntaxKind.ImportEqualsDeclaration, -]; - -function updateResult(statements: readonly ts.Statement[], params: UpdateParams, result: CollectingResult): void { - // contains a set of modules that were visited already - // can be used to prevent infinite recursion in updating results in re-exports - const visitedModules = new Set(); - - function updateResultForExternalExport(exportAssignment: ts.ExportAssignment | ts.ExportDeclaration): void { - // 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 params.getDeclarationsForExportedValues(exportAssignment)) { - let exportedDeclarations: readonly ts.Statement[] = []; - - if (ts.isModuleDeclaration(declaration)) { - if (declaration.body !== undefined && ts.isModuleBlock(declaration.body)) { - const referencedModule = getReferencedModuleInfo(declaration, params); - if (referencedModule !== null) { - if (visitedModules.has(referencedModule.fileName)) { - continue; + if (ts.isImportEqualsDeclaration(st)) { + if (areDeclarationSame(statement, st)) { + importItem.requireImports.add(collisionsResolver.addTopLevelIdentifier(st.name)); } - visitedModules.add(referencedModule.fileName); + return; } - exportedDeclarations = declaration.body.statements; - } - } else { - exportedDeclarations = [declaration as unknown as ts.Statement]; - } + const importClause = st.importClause as ts.ImportClause; + if (importClause.name !== undefined && areDeclarationSame(statement, importClause)) { + // import name from 'module'; + importItem.defaultImports.add(collisionsResolver.addTopLevelIdentifier(importClause.name)); + } - updateResultImpl(exportedDeclarations); + if (importClause.namedBindings !== undefined) { + if (ts.isNamedImports(importClause.namedBindings)) { + // import { El1, El2 as ImportedName } from 'module'; + 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.add(newLocalName === importedName ? importedName : `${importedName} as ${newLocalName}`); + }); + } else { + // import * as name from 'module'; + importItem.starImports.add(collisionsResolver.addTopLevelIdentifier(importClause.namedBindings.name)); + } + } + }); + }); } - } - - // eslint-disable-next-line complexity - function updateResultImpl(statementsToProcess: readonly ts.Statement[]): void { - for (const statement of statementsToProcess) { - // we should skip import statements - if (skippedNodes.indexOf(statement.kind) !== -1) { - continue; - } - - if (isDeclareModule(statement)) { - updateResultForModuleDeclaration(statement, params, result); - // if a statement is `declare module "module" {}` then don't process it below - // as it is handled already in `updateResultForModuleDeclaration` - // but if it is `declare module Module {}` then it can be used in types and imports - // so in this case it needs to be checked for "usages" below - if (ts.isStringLiteral(statement.name)) { - continue; + // eslint-disable-next-line prefer-arrow/prefer-arrow-functions + function getGlobalSymbolsUsingSymbol(symbol: ts.Symbol): ts.Symbol[] { + return Array.from(typesUsageEvaluator.getSymbolsUsingSymbol(symbol) ?? []).filter((usedInSymbol: ts.Symbol) => { + if (usedInSymbol.escapedName !== ts.InternalSymbolName.Global) { + return false; } - } - - if (params.currentModule.type === ModuleType.ShouldBeUsedForModulesOnly) { - continue; - } - if (isDeclareGlobalStatement(statement) && params.shouldDeclareGlobalBeInlined(params.currentModule, statement)) { - result.statements.push(statement); - continue; - } + return getDeclarationsForSymbol(usedInSymbol).some((decl: ts.Declaration) => { + const closestModuleLike = getClosestSourceFileLikeNode(decl); + const moduleInfo = getModuleLikeModuleInfo(closestModuleLike, criteria, typeChecker); + return moduleInfo.type === ModuleType.ShouldBeInlined; + }); + }); + } - if (ts.isExportDeclaration(statement)) { - if (!params.currentModule.isExternal) { - continue; + function isNodeUsed(node: ts.Node): boolean { + if (isNodeNamedDeclaration(node) || ts.isSourceFile(node)) { + const nodeSymbol = getNodeSymbol(node, typeChecker); + if (nodeSymbol === null) { + return false; } - // `export * from` - if (statement.exportClause === undefined) { - updateResultForExternalExport(statement); - continue; + const nodeUsedByDirectExports = rootFileExportSymbols.some((rootExport: ts.Symbol) => typesUsageEvaluator.isSymbolUsedBySymbol(nodeSymbol, rootExport)); + if (nodeUsedByDirectExports) { + return true; } - // `export { val }` - if (ts.isNamedExports(statement.exportClause) && params.currentModule.type === ModuleType.ShouldBeImported) { - updateImportsForStatement(statement, params, result); - continue; - } + return inlineDeclareGlobals && getGlobalSymbolsUsingSymbol(nodeSymbol).length !== 0; + } else if (ts.isVariableStatement(node)) { + return node.declarationList.declarations.some((declaration: ts.VariableDeclaration) => { + return isNodeUsed(declaration); + }); + } else if (ts.isExportDeclaration(node) && node.exportClause !== undefined && ts.isNamespaceExport(node.exportClause)) { + return isNodeUsed(node.exportClause); } - if (ts.isExportAssignment(statement) && statement.isExportEquals && params.currentModule.isExternal) { - updateResultForExternalExport(statement); - continue; - } + return false; + } - if (!params.isStatementUsed(statement)) { - continue; + function shouldNodeBeImported(node: ts.NamedDeclaration): boolean { + const nodeSymbol = getNodeSymbol(node, typeChecker); + if (nodeSymbol === null) { + return false; } - switch (params.currentModule.type) { - case ModuleType.ShouldBeReferencedAsTypes: - addTypesReference(params.currentModule.typesLibraryName, result.typesReferences); - break; + const isSymbolDeclaredInDefaultLibrary = getDeclarationsForSymbol(nodeSymbol).some( + (declaration: ts.Declaration) => program.isSourceFileDefaultLibrary(declaration.getSourceFile()) + ); + if (isSymbolDeclaredInDefaultLibrary) { + // we shouldn't import a node declared in the default library (such dom, es2015) + // yeah, actually we should check that node is declared only in the default lib + // but it seems we can check that at least one declaration is from default lib + // to treat the node as un-importable + // because we can't re-export declared somewhere else node with declaration merging + + // also, if some lib file will not be added to the project + // for example like it is described in the react declaration file (e.g. React Native) + // then here we still have a bug with "importing global declaration from a package" + // (see https://github.com/timocov/dts-bundle-generator/issues/71) + // but I don't think it is a big problem for now + // and it's possible that it will be fixed in https://github.com/timocov/dts-bundle-generator/issues/59 + return false; + } - case ModuleType.ShouldBeImported: - updateImportsForStatement(statement, params, result); - break; + const symbolsDeclarations = getDeclarationsForSymbol(nodeSymbol); - case ModuleType.ShouldBeInlined: - result.statements.push(statement); - break; + // if all declarations of the symbol are in modules that should be inlined then this symbol must be inlined, not imported + const shouldSymbolBeInlined = symbolsDeclarations.every( + (decl: ts.Declaration) => getModuleLikeModuleInfo( + getClosestSourceFileLikeNode(decl), + criteria, + typeChecker + ).type === ModuleType.ShouldBeInlined + ); + if (shouldSymbolBeInlined) { + return false; } + + return getExportedSymbolsUsingSymbol(nodeSymbol).length !== 0; } - } - updateResultImpl(statements); -} + function getExportedSymbolsUsingStatement(node: ts.NamedDeclaration): readonly ts.Symbol[] { + const nodeSymbol = getNodeSymbol(node, typeChecker); + if (nodeSymbol === null) { + return []; + } -// eslint-disable-next-line complexity -function updateResultForRootSourceFile(statements: readonly ts.Statement[], params: UpdateParams, result: CollectingResult): void { - function isReExportFromImportableModule(statement: ts.Statement): boolean { - if (!ts.isExportDeclaration(statement)) { - return false; + return getExportedSymbolsUsingSymbol(nodeSymbol); } - const resolvedModule = params.resolveReferencedModule(statement); - if (resolvedModule === null) { - return false; - } + function getExportedSymbolsUsingSymbol(nodeSymbol: ts.Symbol): readonly ts.Symbol[] { + const symbolsUsingNode = typesUsageEvaluator.getSymbolsUsingSymbol(nodeSymbol); + if (symbolsUsingNode === null) { + throw new Error('Something went wrong - value cannot be null'); + } - return params.getModuleInfo(resolvedModule).type === ModuleType.ShouldBeImported; - } + return [ + // symbols which are used in types directly + ...Array.from(symbolsUsingNode).filter((symbol: ts.Symbol) => { + const symbolsDeclarations = getDeclarationsForSymbol(symbol); + if (symbolsDeclarations.length === 0 || symbolsDeclarations.every((decl: ts.Declaration) => { + // we need to make sure that at least 1 declaration is inlined + return getModuleLikeModuleInfo(getClosestSourceFileLikeNode(decl), criteria, typeChecker).type !== ModuleType.ShouldBeInlined; + })) { + return false; + } + + return rootFileExportSymbols.some((rootSymbol: ts.Symbol) => typesUsageEvaluator.isSymbolUsedBySymbol(symbol, rootSymbol)); + }), + // symbols which are used in global types i.e. in `declare global`s + ...(inlineDeclareGlobals ? getGlobalSymbolsUsingSymbol(nodeSymbol) : []), + ]; + } - updateResult(statements, params, result); + function areDeclarationSame(left: ts.NamedDeclaration, right: ts.NamedDeclaration): boolean { + const leftSymbols = splitTransientSymbol(getNodeSymbol(left, typeChecker) as ts.Symbol, typeChecker); + const rightSymbols = splitTransientSymbol(getNodeSymbol(right, typeChecker) as ts.Symbol, typeChecker); - // add skipped by `updateResult` exports - for (const statement of statements) { - // "export =" or "export {} from 'importable-package'" - if (ts.isExportAssignment(statement) && statement.isExportEquals || isReExportFromImportableModule(statement)) { - result.statements.push(statement); - continue; + return leftSymbols.some((leftSymbol: ts.Symbol) => rightSymbols.includes(leftSymbol)); } - // "export default" - if (ts.isExportAssignment(statement) && !statement.isExportEquals) { - // `export default 123`, `export default "str"` - if (!ts.isIdentifier(statement.expression)) { - result.statements.push(statement); - continue; + function getDeclarationsForExportedValues(exp: ts.ExportAssignment | ts.ExportDeclaration): ts.Declaration[] { + const nodeForSymbol = ts.isExportAssignment(exp) ? exp.expression : exp.moduleSpecifier; + if (nodeForSymbol === undefined) { + return []; } - const originalName = params.resolveIdentifier(statement.expression); - if (originalName === undefined) { - continue; + const symbolForExpression = typeChecker.getSymbolAtLocation(nodeForSymbol); + if (symbolForExpression === undefined) { + return []; } - result.renamedExports.push(`${originalName} as default`); - continue; + const symbol = getActualSymbol(symbolForExpression, typeChecker); + return getDeclarationsForSymbol(symbol); } - // export { foo, bar, baz as fooBar } - if (ts.isExportDeclaration(statement) && statement.exportClause !== undefined && ts.isNamedExports(statement.exportClause)) { - for (const exportItem of statement.exportClause.elements) { - const originalName = params.resolveIdentifier(exportItem.name); - if (originalName === undefined) { + function syncExportsWithRenames(): void { + for (const exp of rootFileExports) { + if (exp.type === ExportType.CommonJS) { + // commonjs will be handled separately where we handle root source files + // as the only way of adding it is to add an explicit export statement in the root source file continue; } - const exportedName = exportItem.name.getText(); - - if (originalName !== exportedName) { - result.renamedExports.push(`${originalName} as ${exportedName}`); + 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 + // in some cases a symbol can be exported but not added to the top-level scope + // for instance in case of re-export from a library `export { Foo } from 'bar'` + // in this case we'll add this re-export differently + continue; } - } - } - } -} - -function getReferencedModuleInfo(moduleDecl: NodeWithReferencedModule, params: UpdateParams): ModuleInfo | null { - const referencedModule = params.resolveReferencedModule(moduleDecl); - if (referencedModule === null) { - return null; - } - - const moduleFilePath = ts.isSourceFile(referencedModule) - ? referencedModule.fileName - : resolveModuleFileName(referencedModule.getSourceFile().fileName, referencedModule.name.text); - - return params.getModuleInfo(moduleFilePath); -} - -function updateResultForModuleDeclaration(moduleDecl: ts.ModuleDeclaration, params: UpdateParams, result: CollectingResult): void { - if (moduleDecl.body === undefined || !ts.isModuleBlock(moduleDecl.body)) { - return; - } - - const referencedModuleInfo = getReferencedModuleInfo(moduleDecl, params); - if (referencedModuleInfo === null) { - return; - } - - // if we have declaration of external module inside internal one - if (!params.currentModule.isExternal && referencedModuleInfo.isExternal) { - // if it's allowed - we need to just add it to result without any processing - if (params.shouldDeclareExternalModuleBeInlined()) { - result.statements.push(moduleDecl); - } - - return; - } - - updateResult( - moduleDecl.body.statements, - { - ...params, - currentModule: referencedModuleInfo, - }, - result - ); -} - -function resolveModuleFileName(currentFileName: string, moduleName: string): string { - return moduleName.startsWith('.') ? fixPath(path.join(currentFileName, '..', moduleName)) : `node_modules/${moduleName}/`; -} -function addTypesReference(library: string, typesReferences: Set): void { - if (!typesReferences.has(library)) { - normalLog(`Library "${library}" will be added via reference directive`); - typesReferences.add(library); - } -} + if (symbolKnownNames.has(exp.exportedName)) { + // an exported symbol is already known with its "exported" name so nothing to do at this point + continue; + } -function updateImportsForStatement(statement: ts.Statement | ts.SourceFile | ts.ExportSpecifier, params: UpdateParams, result: CollectingResult): void { - if (params.currentModule.type !== ModuleType.ShouldBeImported) { - return; - } - - const statementsToImport = ts.isVariableStatement(statement) - ? statement.declarationList.declarations - : ts.isExportDeclaration(statement) && statement.exportClause !== undefined - ? ts.isNamespaceExport(statement.exportClause) - ? [statement.exportClause] - : statement.exportClause.elements - : [statement]; - - for (const statementToImport of statementsToImport) { - if (params.shouldStatementBeImported(statementToImport as ts.DeclarationStatement)) { - addImport(statementToImport as ts.DeclarationStatement, params, result.imports); - - // 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 = params.getModuleInfo(sourceFile.fileName); - if (moduleInfo.type === ModuleType.ShouldBeReferencedAsTypes) { - addTypesReference(moduleInfo.typesLibraryName, result.typesReferences); + // in case if this symbol isn't known yet we need to add it via renamed export + // we assume that all known names are equal so we can use any (first) + // usually all "local" names should have only one known name + // but having multiple names is possible with imports - you can import the same node with different names + // and we want to preserve the source input as much as we can that's why we re-use them + const symbolName = Array.from(symbolKnownNames)[0]; + collectionResult.renamedExports.push(`${symbolName} as ${exp.exportedName}`); } } - } -} - -function getDeclarationUsagesSourceFiles( - declaration: ts.NamedDeclaration, - rootFileExports: readonly ts.Symbol[], - typesUsageEvaluator: TypesUsageEvaluator, - typeChecker: ts.TypeChecker, - criteria: ModuleCriteria, - withGlobals: boolean -): Set { - return new Set( - getExportedSymbolsUsingStatement(declaration, rootFileExports, typesUsageEvaluator, typeChecker, criteria, withGlobals) - .map((symbol: ts.Symbol) => getDeclarationsForSymbol(symbol)) - .reduce((acc: ts.Declaration[], val: ts.Declaration[]) => acc.concat(val), []) - .map(getClosestModuleLikeNode) - ); -} - -function getImportModuleName(imp: ts.ImportEqualsDeclaration | ts.ImportDeclaration): string | null { - if (ts.isImportDeclaration(imp)) { - const importClause = imp.importClause; - if (importClause === undefined) { - return null; - } - - return (imp.moduleSpecifier as ts.StringLiteral).text; - } - - if (ts.isExternalModuleReference(imp.moduleReference)) { - if (!ts.isStringLiteral(imp.moduleReference.expression)) { - warnLog(`Cannot handle non string-literal-like import expression: ${imp.moduleReference.expression.getText()}`); - return null; - } - - return imp.moduleReference.expression.text; - } - return null; -} + for (const sourceFile of sourceFiles) { + verboseLog(`\n\n======= Preparing file: ${sourceFile.fileName} =======`); -function addImport(statement: ts.DeclarationStatement | ts.SourceFile, params: UpdateParams, imports: CollectingResult['imports']): void { - if (!ts.isSourceFile(statement) && statement.name === undefined) { - throw new Error(`Import/usage unnamed declaration: ${statement.getText()}`); - } + const prevStatementsCount = collectionResult.statements.length; + const updateFn = sourceFile === rootSourceFile ? updateResultForRootModule : updateResultForAnyModule; + const currentModule = getFileModuleInfo(sourceFile.fileName, criteria); - params.getDeclarationUsagesSourceFiles(statement).forEach((sourceFile: ts.SourceFile | ts.ModuleDeclaration) => { - const sourceFileStatements = ts.isSourceFile(sourceFile) - ? sourceFile.statements - : (sourceFile.body as ts.ModuleBlock).statements; + updateFn(sourceFile.statements, currentModule); - sourceFileStatements.forEach((st: ts.Statement) => { - if (!ts.isImportEqualsDeclaration(st) && !ts.isImportDeclaration(st)) { - return; + // handle `import * as module` usage if it's used as whole module + if (currentModule.type === ModuleType.ShouldBeImported && isNodeUsed(sourceFile)) { + updateImportsForStatement(sourceFile); } - const importModuleSpecifier = getImportModuleName(st); - if (importModuleSpecifier === null) { - return; + if (collectionResult.statements.length === prevStatementsCount) { + verboseLog(`No output for file: ${sourceFile.fileName}`); } + } - const referencedModuleInfo = getReferencedModuleInfo(st, params); - // if a referenced module should be inlined we can just ignore it - if (referencedModuleInfo === null || referencedModuleInfo.type !== ModuleType.ShouldBeImported) { - return; + if (entryConfig.failOnClass) { + const classes = collectionResult.statements.filter(ts.isClassDeclaration); + if (classes.length !== 0) { + const classesNames = classes.map((c: ts.ClassDeclaration) => c.name === undefined ? 'anonymous class' : c.name.text); + throw new Error(`${classes.length} class statement(s) are found in generated dts: ${classesNames.join(', ')}`); } + } - let importItem = imports.get(importModuleSpecifier); - if (importItem === undefined) { - importItem = { - defaultImports: new Set(), - namedImports: new Set(), - starImports: new Set(), - requireImports: new Set(), - }; + syncExportsWithRenames(); - imports.set(importModuleSpecifier, importItem); - } - - if (ts.isImportEqualsDeclaration(st)) { - if (params.areDeclarationSame(statement, st)) { - importItem.requireImports.add(st.name.text); - } + // by default this option should be enabled + const exportReferencedTypes = outputOptions.exportReferencedTypes !== false; - return; - } + return generateOutput( + { + ...collectionResult, + resolveIdentifierName: (identifier: ts.Identifier | ts.QualifiedName | ts.PropertyAccessEntityNameExpression): string | null => { + if (ts.isIdentifier(identifier)) { + return collisionsResolver.resolveReferencedIdentifier(identifier); + } else { + return collisionsResolver.resolveReferencedQualifiedName(identifier); + } + }, + shouldStatementHasExportKeyword: (statement: ts.Statement) => { + const statementExports = getExportsForStatement(rootFileExports, typeChecker, statement); - const importClause = st.importClause as ts.ImportClause; - if (importClause.name !== undefined && params.areDeclarationSame(statement, importClause)) { - // import name from 'module'; - importItem.defaultImports.add(importClause.name.text); - } + // If true, then no direct export was found. That means that node might have + // an export keyword (like interface, type, etc) otherwise, if there are + // only re-exports with renaming (like export { foo as bar }) we don't need + // to put export keyword for this statement because we'll re-export it in the way + // const hasStatementDefaultKeyword = hasNodeModifier(statement, ts.SyntaxKind.DefaultKeyword); + let result = statementExports.length === 0 || statementExports.find((exp: SourceFileExport) => { + if (ts.isVariableStatement(statement)) { + for (const variableDeclaration of statement.declarationList.declarations) { + if (ts.isIdentifier(variableDeclaration.name)) { + const resolvedName = collisionsResolver.resolveReferencedIdentifier(variableDeclaration.name); + if (exp.exportedName === resolvedName) { + return true; + } + } + + // it seems that the compiler doesn't produce anything else (e.g. binding elements) in declaration files + // but it is still possible to write such code manually + // this feels like quite rare case so no support for now + } - interface ImportSpecifierInternal extends ts.ImportSpecifier { - // fallback to support TS versions without type-only imports/exports - isTypeOnly: boolean; - } + return false; + } - if (importClause.namedBindings !== undefined) { - if (ts.isNamedImports(importClause.namedBindings)) { - // import { El1, El2 } from 'module'; - importClause.namedBindings.elements - .filter(params.areDeclarationSame.bind(params, statement)) - .forEach((specifier: ts.ImportSpecifier) => { - let importName = specifier.getText(); - if ((specifier as ImportSpecifierInternal).isTypeOnly) { - // let's fallback all the imports to ones without "type" specifier - importName = importName.replace(/^(\s*type\s+)/g, ''); + if (isNodeNamedDeclaration(statement)) { + const nodeName = getNodeName(statement); + if (nodeName === undefined) { + throw new Error(`Cannot find node name ${statement.getText()}`); } - (importItem as ModuleImportsSet).namedImports.add(importName); - }); - } else { - // import * as name from 'module'; - importItem.starImports.add(importClause.namedBindings.name.getText()); - } - } - }); - }); -} + const resolvedName = collisionsResolver.resolveReferencedIdentifier(nodeName as ts.Identifier); + return exp.exportedName === resolvedName; + } -function getGlobalSymbolsUsingSymbol( - symbol: ts.Symbol, - typesUsageEvaluator: TypesUsageEvaluator, - criteria: ModuleCriteria -): ts.Symbol[] { - return Array.from(typesUsageEvaluator.getSymbolsUsingSymbol(symbol) ?? []).filter((usedInSymbol: ts.Symbol) => { - if (usedInSymbol.escapedName !== ts.InternalSymbolName.Global) { - return false; - } + return false; + }) !== undefined; - return getDeclarationsForSymbol(usedInSymbol).some((decl: ts.Declaration) => { - const closestModuleLike = getClosestModuleLikeNode(decl); - const moduleInfo = getModuleLikeInfo(closestModuleLike, criteria); - return moduleInfo.type === ModuleType.ShouldBeInlined; - }); - }); -} + // "direct export" means export from the root source file + // e.g. classes/functions/etc must be exported from the root source file to have an "export" keyword + // by default interfaces/types are exported even if they aren't directly exported (e.g. when they are referenced by other types) + // but if `exportReferencedTypes` option is disabled we have to check direct export for them either + const onlyDirectlyExportedShouldBeExported = !exportReferencedTypes + || ts.isClassDeclaration(statement) + || (ts.isEnumDeclaration(statement) && !hasNodeModifier(statement, ts.SyntaxKind.ConstKeyword)) + || ts.isFunctionDeclaration(statement) + || ts.isVariableStatement(statement) + || ts.isModuleDeclaration(statement); -function isNodeUsed( - node: ts.Node, - rootFileExports: readonly ts.Symbol[], - typesUsageEvaluator: TypesUsageEvaluator, - typeChecker: ts.TypeChecker, - criteria: ModuleCriteria, - withGlobals: boolean -): boolean { - if (isNodeNamedDeclaration(node) || ts.isSourceFile(node)) { - const nodeSymbol = getNodeSymbol(node, typeChecker); - if (nodeSymbol === null) { - return false; - } + if (onlyDirectlyExportedShouldBeExported) { + // "valuable" statements must be re-exported from root source file + // to having export keyword in declaration file + result = result && statementExports.length !== 0; + } else if (isAmbientModule(statement) || ts.isExportDeclaration(statement)) { + result = false; + } - const nodeUsedByDirectExports = rootFileExports.some((rootExport: ts.Symbol) => typesUsageEvaluator.isSymbolUsedBySymbol(nodeSymbol, rootExport)); - if (nodeUsedByDirectExports) { - return true; - } + return result; + }, + needStripConstFromConstEnum: (constEnum: ts.EnumDeclaration) => { + if (!program.getCompilerOptions().preserveConstEnums || !outputOptions.respectPreserveConstEnum) { + return false; + } - return withGlobals && getGlobalSymbolsUsingSymbol(nodeSymbol, typesUsageEvaluator, criteria).length !== 0; - } else if (ts.isVariableStatement(node)) { - return node.declarationList.declarations.some((declaration: ts.VariableDeclaration) => { - return isNodeUsed(declaration, rootFileExports, typesUsageEvaluator, typeChecker, criteria, withGlobals); - }); - } else if (ts.isExportDeclaration(node) && node.exportClause !== undefined && ts.isNamespaceExport(node.exportClause)) { - return isNodeUsed(node.exportClause, rootFileExports, typesUsageEvaluator, typeChecker, criteria, withGlobals); - } + const enumSymbol = getNodeSymbol(constEnum, typeChecker); + if (enumSymbol === null) { + return false; + } - return false; -} + return rootFileExportSymbols.includes(enumSymbol); + }, + needStripImportFromImportTypeNode: (node: ts.ImportTypeNode) => { + if (node.qualifier === undefined) { + return false; + } -// eslint-disable-next-line max-params -function shouldNodeBeImported( - node: ts.NamedDeclaration, - rootFileExports: readonly ts.Symbol[], - typesUsageEvaluator: TypesUsageEvaluator, - typeChecker: ts.TypeChecker, - isDefaultLibrary: (sourceFile: ts.SourceFile) => boolean, - criteria: ModuleCriteria, - withGlobals: boolean -): boolean { - const nodeSymbol = getNodeSymbol(node, typeChecker); - if (nodeSymbol === null) { - return false; - } - - const symbolDeclarations = getDeclarationsForSymbol(nodeSymbol); - const isSymbolDeclaredInDefaultLibrary = symbolDeclarations.some( - (declaration: ts.Declaration) => isDefaultLibrary(declaration.getSourceFile()) - ); - if (isSymbolDeclaredInDefaultLibrary) { - // we shouldn't import a node declared in the default library (such dom, es2015) - // yeah, actually we should check that node is declared only in the default lib - // but it seems we can check that at least one declaration is from default lib - // to treat the node as un-importable - // because we can't re-export declared somewhere else node with declaration merging - - // also, if some lib file will not be added to the project - // for example like it is described in the react declaration file (e.g. React Native) - // then here we still have a bug with "importing global declaration from a package" - // (see https://github.com/timocov/dts-bundle-generator/issues/71) - // but I don't think it is a big problem for now - // and it's possible that it will be fixed in https://github.com/timocov/dts-bundle-generator/issues/59 - return false; - } - - return getExportedSymbolsUsingStatement( - node, - rootFileExports, - typesUsageEvaluator, - typeChecker, - criteria, - withGlobals - ).length !== 0; -} + if (!ts.isLiteralTypeNode(node.argument) || !ts.isStringLiteral(node.argument.literal)) { + return false; + } -function getExportedSymbolsUsingStatement( - node: ts.NamedDeclaration, - rootFileExports: readonly ts.Symbol[], - typesUsageEvaluator: TypesUsageEvaluator, - typeChecker: ts.TypeChecker, - criteria: ModuleCriteria, - withGlobals: boolean -): readonly ts.Symbol[] { - const nodeSymbol = getNodeSymbol(node, typeChecker); - if (nodeSymbol === null) { - return []; - } - - const symbolsUsingNode = typesUsageEvaluator.getSymbolsUsingSymbol(nodeSymbol); - if (symbolsUsingNode === null) { - throw new Error('Something went wrong - value cannot be null'); - } - - return [ - // symbols which are used in types directly - ...Array.from(symbolsUsingNode).filter((symbol: ts.Symbol) => { - const symbolsDeclarations = getDeclarationsForSymbol(symbol); - if (symbolsDeclarations.length === 0 || symbolsDeclarations.every((decl: ts.Declaration) => { - // we need to make sure that at least 1 declaration is inlined - return getModuleLikeInfo(getClosestModuleLikeNode(decl), criteria).type !== ModuleType.ShouldBeInlined; - })) { - return false; + return getReferencedModuleInfo(node, criteria, typeChecker)?.type === ModuleType.ShouldBeInlined; + }, + }, + { + sortStatements: outputOptions.sortNodes, + umdModuleName: outputOptions.umdModuleName, + noBanner: outputOptions.noBanner, } - - return rootFileExports.some((rootSymbol: ts.Symbol) => typesUsageEvaluator.isSymbolUsedBySymbol(symbol, rootSymbol)); - }), - // symbols which are used in global types i.e. in `declare global`s - ...(withGlobals ? getGlobalSymbolsUsingSymbol(nodeSymbol, typesUsageEvaluator, criteria) : []), - ]; -} - -function getModuleLikeInfo(moduleLike: ts.SourceFile | ts.ModuleDeclaration, criteria: ModuleCriteria): ModuleInfo { - const fileName = ts.isSourceFile(moduleLike) - ? moduleLike.fileName - : resolveModuleFileName(moduleLike.getSourceFile().fileName, moduleLike.name.text); - - return getModuleInfo(fileName, criteria); + ); + }); } diff --git a/src/collisions-resolver.ts b/src/collisions-resolver.ts new file mode 100644 index 0000000..328c820 --- /dev/null +++ b/src/collisions-resolver.ts @@ -0,0 +1,182 @@ +import * as ts from 'typescript'; + +import { + getActualSymbol, + getDeclarationNameSymbol, +} from './helpers/typescript'; +import { verboseLog } from './logger'; + +const renamingSupportedSymbols: readonly ts.SymbolFlags[] = [ + ts.SymbolFlags.Alias, + ts.SymbolFlags.BlockScopedVariable, + ts.SymbolFlags.Class, + ts.SymbolFlags.Enum, + ts.SymbolFlags.Function, + ts.SymbolFlags.Interface, + ts.SymbolFlags.NamespaceModule, + ts.SymbolFlags.TypeAlias, + ts.SymbolFlags.ValueModule, +]; + +export interface ResolverIdentifier { + name: string; + identifier?: ts.Identifier; +} + +/** + * A class that holds information about top-level scoped names and allows to get collision-free names in one occurred. + */ +export class CollisionsResolver { + private typeChecker: ts.TypeChecker; + + private collisionsMap: Map = new Map(); + private generatedNames: Map> = new Map(); + + public constructor(typeChecker: ts.TypeChecker) { + this.typeChecker = typeChecker; + } + + /** + * Adds (or "registers") a top-level {@link identifier} (which takes a top-level scope name to use). + */ + public addTopLevelIdentifier(identifier: ts.Identifier | ts.DefaultKeyword): string { + const symbol = getDeclarationNameSymbol(identifier, this.typeChecker); + if (symbol === null) { + throw new Error(`Something went wrong - cannot find a symbol for top-level identifier ${identifier.getText()} (from ${identifier.parent.parent.getText()})`); + } + + const newLocalName = this.registerSymbol(symbol, identifier.getText()); + if (newLocalName === null) { + throw new Error(`Something went wrong - a symbol ${symbol.escapedName} for top-level identifier ${identifier.getText()} cannot be renamed`); + } + + return newLocalName; + } + + /** + * Returns a set of all already registered names for a given {@link symbol}. + */ + public namesForSymbol(symbol: ts.Symbol): Set { + return this.generatedNames.get(getActualSymbol(symbol, this.typeChecker)) || new Set(); + } + + /** + * 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`. + */ + public resolveReferencedIdentifier(referencedIdentifier: ts.Identifier): string | null { + const identifierSymbol = getDeclarationNameSymbol(referencedIdentifier, this.typeChecker); + if (identifierSymbol === null) { + // that's fine if an identifier doesn't have a symbol + // it could be in cases like for `prop` in `declare function func({ prop: prop3 }?: InterfaceName): TypeName;` + 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; + } + + return Array.from(namesForSymbol)[0] || null; + } + + /** + * Similar to {@link resolveReferencedIdentifier}, but works with qualified names (Ns.Ns1.Interface). + * The main point of this resolver is that it might change the first part of the qualifier only (as it drives uniqueness of a name). + */ + public resolveReferencedQualifiedName(referencedIdentifier: ts.QualifiedName | ts.PropertyAccessEntityNameExpression): string | null { + let topLevelIdentifier: ts.Identifier | ts.QualifiedName | ts.PropertyAccessEntityNameExpression = referencedIdentifier; + + if (ts.isQualifiedName(topLevelIdentifier) || ts.isPropertyAccessExpression(topLevelIdentifier)) { + let leftmostIdentifier = ts.isQualifiedName(topLevelIdentifier) ? topLevelIdentifier.left : topLevelIdentifier.expression; + + while (ts.isQualifiedName(leftmostIdentifier) || ts.isPropertyAccessExpression(leftmostIdentifier)) { + leftmostIdentifier = ts.isQualifiedName(leftmostIdentifier) ? leftmostIdentifier.left : leftmostIdentifier.expression; + } + + topLevelIdentifier = leftmostIdentifier; + } + + const topLevelName = this.resolveReferencedIdentifier(topLevelIdentifier); + if (topLevelName === null) { + // that's fine if we don't have a name for this top-level symbol + // it simply means that this symbol type might not be supported for renaming + // at this point the top-level identifier isn't registered yet + // but it is possible that the full qualified name is registered so we can use its replacement instead + // it is possible in cases where you use `import * as nsName` for internal modules + // so `nsName.Interface` will be resolved to `Interface` (or any other name that `Interface` was registered with) + const identifierSymbol = getDeclarationNameSymbol(referencedIdentifier, this.typeChecker); + if (identifierSymbol === null) { + // that's fine if an identifier doesn't have a symbol + // it could be in cases like for `prop` in `declare function func({ prop: prop3 }?: InterfaceName): TypeName;` + return null; + } + + const namesForSymbol = this.namesForSymbol(identifierSymbol); + if (namesForSymbol.size !== 0) { + // if the set of already registered names contains the one that is requested then lets use it + return Array.from(namesForSymbol)[0]; + } + + // if it is not registered - just skip it + return null; + } + + // for nodes that we have to import we need to add an imported value to the collisions map + // so it will not overlap with other imports/inlined nodes + const identifierParts = referencedIdentifier.getText().split('.'); + + // update top level part as it could get renamed above + identifierParts[0] = topLevelName; + + return identifierParts.join('.'); + } + + 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 + verboseLog(`Symbol ${identifierSymbol.escapedName} cannot be renamed because its flag (${identifierSymbol.flags}) isn't supported`); + return null; + } + + if (identifierSymbol.flags & ts.SymbolFlags.NamespaceModule && identifierSymbol.escapedName === ts.InternalSymbolName.Global) { + // no need to rename `declare global` namespaces + return null; + } + + let symbolName = preferredName; + if (symbolName === 'default') { + // this is special case as an identifier cannot be named `default` because of es6 syntax + // so lets fallback to some valid name + symbolName = '_default'; + } + + const collisionsKey = symbolName; + let nameSymbols = this.collisionsMap.get(collisionsKey); + if (nameSymbols === undefined) { + nameSymbols = [identifierSymbol]; + this.collisionsMap.set(collisionsKey, nameSymbols); + } + + let symbolIndex = nameSymbols.indexOf(identifierSymbol); + if (symbolIndex === -1) { + nameSymbols.push(identifierSymbol); + symbolIndex = nameSymbols.length - 1; + } + + const newName = symbolIndex === 0 ? symbolName : `${symbolName}$${symbolIndex}`; + + let symbolNames = this.generatedNames.get(identifierSymbol); + if (symbolNames === undefined) { + symbolNames = new Set(); + this.generatedNames.set(identifierSymbol, symbolNames); + } + + symbolNames.add(newName); + + return newName; + } +} diff --git a/src/generate-output.ts b/src/generate-output.ts index d1e1951..54b484d 100644 --- a/src/generate-output.ts +++ b/src/generate-output.ts @@ -1,7 +1,7 @@ import * as ts from 'typescript'; import { packageVersion } from './helpers/package-version'; -import { getModifiers, modifiersToMap, recreateRootLevelNodeWithModifiers } from './helpers/typescript'; +import { getModifiers, getNodeName, modifiersToMap, recreateRootLevelNodeWithModifiers } from './helpers/typescript'; export interface ModuleImportsSet { defaultImports: Set; @@ -24,9 +24,9 @@ export interface NeedStripDefaultKeywordResult { export interface OutputHelpers { shouldStatementHasExportKeyword(statement: ts.Statement): boolean; - needStripDefaultKeywordForStatement(statement: ts.Statement): NeedStripDefaultKeywordResult; needStripConstFromConstEnum(constEnum: ts.EnumDeclaration): boolean; needStripImportFromImportTypeNode(importType: ts.ImportTypeNode): boolean; + resolveIdentifierName(identifier: ts.Identifier | ts.QualifiedName | ts.PropertyAccessEntityNameExpression): string | null; } export interface OutputOptions { @@ -125,6 +125,9 @@ function compareStatementText(a: StatementText, b: StatementText): number { function getStatementText(statement: ts.Statement, includeSortingValue: boolean, helpers: OutputHelpers): StatementText { const shouldStatementHasExportKeyword = helpers.shouldStatementHasExportKeyword(statement); + // re-export statements do not contribute to top-level names scope so we don't need to resolve their identifiers + const needResolveIdentifiers = !ts.isExportDeclaration(statement) || statement.moduleSpecifier === undefined; + const printer = ts.createPrinter( { newLine: ts.NewLineKind.LineFeed, @@ -133,6 +136,53 @@ function getStatementText(statement: ts.Statement, includeSortingValue: boolean, { // eslint-disable-next-line complexity substituteNode: (hint: ts.EmitHint, node: ts.Node) => { + if (node.parent === undefined) { + return node; + } + + if (needResolveIdentifiers) { + if (ts.isPropertyAccessExpression(node) || ts.isQualifiedName(node)) { + const resolvedName = helpers.resolveIdentifierName(node as ts.PropertyAccessEntityNameExpression | ts.QualifiedName); + if (resolvedName !== null && resolvedName !== node.getText()) { + const identifiers = resolvedName.split('.'); + + let result: ts.PropertyAccessExpression | ts.QualifiedName | 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]) + ); + } + } + + return result; + } + + return node; + } + + if (ts.isIdentifier(node)) { + // PropertyAccessExpression and QualifiedName are handled above already + if (ts.isPropertyAccessExpression(node.parent) || ts.isQualifiedName(node.parent)) { + return node; + } + + const resolvedName = helpers.resolveIdentifierName(node); + if (resolvedName !== null && resolvedName !== node.getText()) { + return ts.factory.createIdentifier(resolvedName); + } + + return node; + } + } + // `import('module').Qualifier` or `typeof import('module').Qualifier` if (ts.isImportTypeNode(node) && node.qualifier !== undefined && helpers.needStripImportFromImportTypeNode(node)) { if (node.isTypeOf) { @@ -156,20 +206,16 @@ function getStatementText(statement: ts.Statement, includeSortingValue: boolean, modifiersMap[ts.SyntaxKind.ConstKeyword] = false; } - let newName: string | undefined; + const nodeName = getNodeName(node); - // strip the `default` keyword from node - if (modifiersMap[ts.SyntaxKind.DefaultKeyword]) { - const needStripDefaultKeywordResult = helpers.needStripDefaultKeywordForStatement(statement); - if (needStripDefaultKeywordResult.needStrip) { - // we need just to remove `default` from any node except class node - // for classes we need to replace `default` with `declare` instead - modifiersMap[ts.SyntaxKind.DefaultKeyword] = false; - if (ts.isClassDeclaration(node)) { - modifiersMap[ts.SyntaxKind.DeclareKeyword] = true; - } + const resolvedStatementName = nodeName !== undefined ? helpers.resolveIdentifierName(nodeName as ts.Identifier) || undefined : undefined; - newName = needStripDefaultKeywordResult.newName; + // strip the `default` keyword from node regardless + if (modifiersMap[ts.SyntaxKind.DefaultKeyword]) { + modifiersMap[ts.SyntaxKind.DefaultKeyword] = false; + if (ts.isClassDeclaration(node)) { + // for classes we need to replace `default` with `declare` instead otherwise it will produce an invalid syntax + modifiersMap[ts.SyntaxKind.DeclareKeyword] = true; } } @@ -193,7 +239,7 @@ function getStatementText(statement: ts.Statement, includeSortingValue: boolean, modifiersMap[ts.SyntaxKind.DeclareKeyword] = true; } - return recreateRootLevelNodeWithModifiers(node, modifiersMap, newName, shouldStatementHasExportKeyword); + return recreateRootLevelNodeWithModifiers(node, modifiersMap, resolvedStatementName, shouldStatementHasExportKeyword); }, } ); diff --git a/src/helpers/typescript.ts b/src/helpers/typescript.ts index 1cb1b67..1654dca 100644 --- a/src/helpers/typescript.ts +++ b/src/helpers/typescript.ts @@ -1,5 +1,7 @@ import * as ts from 'typescript'; +import { warnLog } from '../logger'; + const namedDeclarationKinds = [ ts.SyntaxKind.InterfaceDeclaration, ts.SyntaxKind.ClassDeclaration, @@ -13,7 +15,7 @@ const namedDeclarationKinds = [ ts.SyntaxKind.ExportSpecifier, ]; -export type NodeName = ts.DeclarationName | ts.DefaultKeyword; +export type NodeName = ts.DeclarationName | ts.DefaultKeyword | ts.QualifiedName | ts.PropertyAccessExpression; export function isNodeNamedDeclaration(node: ts.Node): node is ts.NamedDeclaration { return namedDeclarationKinds.indexOf(node.kind) !== -1; @@ -24,7 +26,7 @@ export function hasNodeModifier(node: ts.Node, modifier: ts.SyntaxKind): boolean return Boolean(modifiers && modifiers.some((nodeModifier: ts.Modifier) => nodeModifier.kind === modifier)); } -function getNodeName(node: ts.Node): NodeName | undefined { +export function getNodeName(node: ts.Node): NodeName | undefined { const nodeName = (node as unknown as ts.NamedDeclaration).name; if (nodeName === undefined) { const modifiers = getModifiers(node); @@ -51,7 +53,7 @@ export function getActualSymbol(symbol: ts.Symbol, typeChecker: ts.TypeChecker): return (typeChecker as TypeCheckerCompat).getMergedSymbol(symbol); } -function getDeclarationNameSymbol(name: NodeName, typeChecker: ts.TypeChecker): ts.Symbol | null { +export function getDeclarationNameSymbol(name: NodeName, typeChecker: ts.TypeChecker): ts.Symbol | null { const symbol = typeChecker.getSymbolAtLocation(name); if (symbol === undefined) { return null; @@ -194,7 +196,7 @@ export function getExportsForSourceFile(typeChecker: ts.TypeChecker, sourceFileS result.forEach((exp: SourceFileExport) => { exp.symbol = getActualSymbol(exp.symbol, typeChecker); - const resolvedIdentifier = resolveIdentifierBySymbol(exp.symbol); + const resolvedIdentifier = resolveDeclarationByIdentifierSymbol(exp.symbol); exp.originalName = resolvedIdentifier?.name !== undefined ? resolvedIdentifier.name.getText() : exp.symbol.escapedName as string; }); @@ -207,10 +209,10 @@ export function resolveIdentifier(typeChecker: ts.TypeChecker, identifier: ts.Id return undefined; } - return resolveIdentifierBySymbol(symbol); + return resolveDeclarationByIdentifierSymbol(symbol); } -function resolveIdentifierBySymbol(identifierSymbol: ts.Symbol): ts.NamedDeclaration | undefined { +function resolveDeclarationByIdentifierSymbol(identifierSymbol: ts.Symbol): ts.NamedDeclaration | undefined { const declarations = getDeclarationsForSymbol(identifierSymbol); if (declarations.length === 0) { return undefined; @@ -510,11 +512,79 @@ export function getNodeSymbol(node: ts.Node, typeChecker: ts.TypeChecker): ts.Sy } export function getClosestModuleLikeNode(node: ts.Node): ts.SourceFile | ts.ModuleDeclaration { + // we need to find a module block and return its module declaration + // we don't need to handle empty modules/modules with jsdoc/etc while (!ts.isModuleBlock(node) && !ts.isSourceFile(node)) { node = node.parent; } + return ts.isSourceFile(node) ? node : node.parent; +} + +export function getClosestSourceFileLikeNode(node: ts.Node): ts.SourceFile | ts.ModuleDeclaration { // we need to find a module block and return its module declaration // we don't need to handle empty modules/modules with jsdoc/etc + while (!(ts.isModuleBlock(node) && ts.isStringLiteral(node.parent.name)) && !ts.isSourceFile(node)) { + node = node.parent; + } + return ts.isSourceFile(node) ? node : node.parent; } + +export type NodeWithReferencedModule = ts.ExportDeclaration | ts.ModuleDeclaration | ts.ImportTypeNode | ts.ImportEqualsDeclaration | ts.ImportDeclaration; + +export function resolveReferencedModule(node: NodeWithReferencedModule, typeChecker: ts.TypeChecker): ts.SourceFile | ts.ModuleDeclaration | null { + let moduleName: ts.Expression | ts.LiteralTypeNode | undefined; + + if (ts.isExportDeclaration(node) || ts.isImportDeclaration(node)) { + moduleName = node.moduleSpecifier; + } else if (ts.isModuleDeclaration(node)) { + moduleName = node.name; + } else if (ts.isImportEqualsDeclaration(node)) { + if (ts.isExternalModuleReference(node.moduleReference)) { + moduleName = node.moduleReference.expression; + } + } else if (ts.isLiteralTypeNode(node.argument) && ts.isStringLiteral(node.argument.literal)) { + moduleName = node.argument.literal; + } + + if (moduleName === undefined) { + return null; + } + + const moduleSymbol = typeChecker.getSymbolAtLocation(moduleName); + if (moduleSymbol === undefined) { + return null; + } + + const symbol = getActualSymbol(moduleSymbol, typeChecker); + if (symbol.valueDeclaration === undefined) { + return null; + } + + return ts.isSourceFile(symbol.valueDeclaration) || ts.isModuleDeclaration(symbol.valueDeclaration) + ? symbol.valueDeclaration + : null; +} + +export function getImportModuleName(imp: ts.ImportEqualsDeclaration | ts.ImportDeclaration): string | null { + if (ts.isImportDeclaration(imp)) { + const importClause = imp.importClause; + if (importClause === undefined) { + return null; + } + + return (imp.moduleSpecifier as ts.StringLiteral).text; + } + + if (ts.isExternalModuleReference(imp.moduleReference)) { + if (!ts.isStringLiteral(imp.moduleReference.expression)) { + warnLog(`Cannot handle non string-literal-like import expression: ${imp.moduleReference.expression.getText()}`); + return null; + } + + return imp.moduleReference.expression.text; + } + + return null; +} diff --git a/src/module-info.ts b/src/module-info.ts index bf4b573..d0fb164 100644 --- a/src/module-info.ts +++ b/src/module-info.ts @@ -1,11 +1,14 @@ import * as path from 'path'; +import * as ts from 'typescript'; + import { getLibraryName, getTypesLibraryName, } from './helpers/node-modules'; import { fixPath } from './helpers/fix-path'; +import { NodeWithReferencedModule, resolveReferencedModule } from './helpers/typescript'; export const enum ModuleType { ShouldBeInlined, @@ -48,10 +51,37 @@ export interface ModuleCriteria { typeRoots?: string[]; } -export function getModuleInfo(fileName: string, criteria: ModuleCriteria): ModuleInfo { +export function getFileModuleInfo(fileName: string, criteria: ModuleCriteria): ModuleInfo { return getModuleInfoImpl(fileName, fileName, criteria); } +export function getReferencedModuleInfo(moduleDecl: NodeWithReferencedModule, criteria: ModuleCriteria, typeChecker: ts.TypeChecker): ModuleInfo | null { + const referencedModule = resolveReferencedModule(moduleDecl, typeChecker); + if (referencedModule === null) { + return null; + } + + const moduleFilePath = ts.isSourceFile(referencedModule) + ? referencedModule.fileName + : resolveModuleFileName(referencedModule.getSourceFile().fileName, referencedModule.name.text); + + return getFileModuleInfo(moduleFilePath, criteria); +} + +export function getModuleLikeModuleInfo(moduleLike: ts.SourceFile | ts.ModuleDeclaration, criteria: ModuleCriteria, typeChecker: ts.TypeChecker): ModuleInfo { + const resolvedModuleLike = ts.isSourceFile(moduleLike) ? moduleLike : resolveReferencedModule(moduleLike, typeChecker) ?? moduleLike; + + const fileName = ts.isSourceFile(resolvedModuleLike) + ? resolvedModuleLike.fileName + : resolveModuleFileName(resolvedModuleLike.getSourceFile().fileName, resolvedModuleLike.name.text); + + return getFileModuleInfo(fileName, criteria); +} + +function resolveModuleFileName(currentFileName: string, moduleName: string): string { + return moduleName.startsWith('.') ? fixPath(path.join(currentFileName, '..', moduleName)) : `node_modules/${moduleName}/`; +} + /** * @param currentFilePath Current file path - can be used to override actual path of module (e.g. with `typeRoots`) * @param originalFileName Original file name of the module diff --git a/tests/e2e/test-cases/export-default-from-entry/output.d.ts b/tests/e2e/test-cases/export-default-from-entry/output.d.ts index 7f56afc..b9b583b 100644 --- a/tests/e2e/test-cases/export-default-from-entry/output.d.ts +++ b/tests/e2e/test-cases/export-default-from-entry/output.d.ts @@ -3,7 +3,11 @@ export interface MyInterface { export type MyType = { [K in keyof T]: string; }; -export default class NewClass implements MyInterface { +declare class NewClass implements MyInterface { } +export { + NewClass as default, +}; + export {}; diff --git a/tests/e2e/test-cases/export-default-from-non-entry/output.d.ts b/tests/e2e/test-cases/export-default-from-non-entry/output.d.ts index c208664..16ec638 100644 --- a/tests/e2e/test-cases/export-default-from-non-entry/output.d.ts +++ b/tests/e2e/test-cases/export-default-from-non-entry/output.d.ts @@ -4,9 +4,13 @@ declare class MyClass { } declare class MyAnotherClass { } -export default class MyNewClass extends MyClass implements MyInterface { +declare class MyNewClass extends MyClass implements MyInterface { } export declare class MyNewClass2 extends MyAnotherClass { } +export { + MyNewClass as default, +}; + export {}; diff --git a/tests/e2e/test-cases/export-default-unnamed-statement/input.ts b/tests/e2e/test-cases/export-default-unnamed-statement/input.ts index 670b06b..a4d6774 100644 --- a/tests/e2e/test-cases/export-default-unnamed-statement/input.ts +++ b/tests/e2e/test-cases/export-default-unnamed-statement/input.ts @@ -7,3 +7,7 @@ export { default as myClass1 } from './class'; export { default as myClass2 } from './class'; export { default as myClass3 } from './another-class'; export { default as myClass4 } from './another-class'; + +export { default as number } from './number'; +export { default as string } from './string'; +export { default as object } from './object'; diff --git a/tests/e2e/test-cases/export-default-unnamed-statement/number.ts b/tests/e2e/test-cases/export-default-unnamed-statement/number.ts new file mode 100644 index 0000000..7f810d3 --- /dev/null +++ b/tests/e2e/test-cases/export-default-unnamed-statement/number.ts @@ -0,0 +1 @@ +export default 0; diff --git a/tests/e2e/test-cases/export-default-unnamed-statement/object.ts b/tests/e2e/test-cases/export-default-unnamed-statement/object.ts new file mode 100644 index 0000000..81a75a9 --- /dev/null +++ b/tests/e2e/test-cases/export-default-unnamed-statement/object.ts @@ -0,0 +1,3 @@ +export default { + type: 'object', +}; \ No newline at end of file diff --git a/tests/e2e/test-cases/export-default-unnamed-statement/output.d.ts b/tests/e2e/test-cases/export-default-unnamed-statement/output.d.ts index d270232..3df19bb 100644 --- a/tests/e2e/test-cases/export-default-unnamed-statement/output.d.ts +++ b/tests/e2e/test-cases/export-default-unnamed-statement/output.d.ts @@ -1,21 +1,29 @@ -declare function __DTS_BUNDLE_GENERATOR__GENERATED_NAME$1(first: number): void; -declare function __DTS_BUNDLE_GENERATOR__GENERATED_NAME$2(second: number): void; -declare class __DTS_BUNDLE_GENERATOR__GENERATED_NAME$3 { +declare function _default(first: number): void; +declare function _default$1(second: number): void; +declare class _default$2 { first: number; } -declare class __DTS_BUNDLE_GENERATOR__GENERATED_NAME$4 { +declare class _default$3 { second: number; } +declare const _default$4: 0; +declare const _default$5: ""; +declare const _default$6: { + type: string; +}; export { - __DTS_BUNDLE_GENERATOR__GENERATED_NAME$1 as myFunc1, - __DTS_BUNDLE_GENERATOR__GENERATED_NAME$1 as myFunc2, - __DTS_BUNDLE_GENERATOR__GENERATED_NAME$2 as myFunc3, - __DTS_BUNDLE_GENERATOR__GENERATED_NAME$2 as myFunc4, - __DTS_BUNDLE_GENERATOR__GENERATED_NAME$3 as myClass1, - __DTS_BUNDLE_GENERATOR__GENERATED_NAME$3 as myClass2, - __DTS_BUNDLE_GENERATOR__GENERATED_NAME$4 as myClass3, - __DTS_BUNDLE_GENERATOR__GENERATED_NAME$4 as myClass4, + _default as myFunc1, + _default as myFunc2, + _default$1 as myFunc3, + _default$1 as myFunc4, + _default$2 as myClass1, + _default$2 as myClass2, + _default$3 as myClass3, + _default$3 as myClass4, + _default$4 as number, + _default$5 as string, + _default$6 as object, }; export {}; diff --git a/tests/e2e/test-cases/export-default-unnamed-statement/string.ts b/tests/e2e/test-cases/export-default-unnamed-statement/string.ts new file mode 100644 index 0000000..08d725c --- /dev/null +++ b/tests/e2e/test-cases/export-default-unnamed-statement/string.ts @@ -0,0 +1 @@ +export default ''; diff --git a/tests/e2e/test-cases/import()-type/my-type.d.ts b/tests/e2e/test-cases/import()-type/my-type.d.ts index 083932b..a007f70 100644 --- a/tests/e2e/test-cases/import()-type/my-type.d.ts +++ b/tests/e2e/test-cases/import()-type/my-type.d.ts @@ -4,4 +4,5 @@ export interface MyType { field3: import('ora').Options; field4: import('./custom-type').GenericType; field5: import('fake-package').Interface; + field6: typeof import('./namespace').Namespace; } diff --git a/tests/e2e/test-cases/import()-type/namespace.d.ts b/tests/e2e/test-cases/import()-type/namespace.d.ts new file mode 100644 index 0000000..fd59c8f --- /dev/null +++ b/tests/e2e/test-cases/import()-type/namespace.d.ts @@ -0,0 +1,3 @@ +export namespace Namespace { + export const baz: number; +} diff --git a/tests/e2e/test-cases/import()-type/output.d.ts b/tests/e2e/test-cases/import()-type/output.d.ts index a1522aa..66f307f 100644 --- a/tests/e2e/test-cases/import()-type/output.d.ts +++ b/tests/e2e/test-cases/import()-type/output.d.ts @@ -8,12 +8,16 @@ declare namespace Namespace { export declare type GenericType = {}; export interface Interface { } +declare namespace Namespace$1 { + export const baz: number; +} export interface MyType { field: CustomType; field2: typeof Namespace; field3: import("ora").Options; field4: GenericType; field5: Interface; + field6: typeof Namespace$1; } export type MySecondType = MyType | number; diff --git a/tests/e2e/test-cases/merged-namespaces/config.ts b/tests/e2e/test-cases/merged-namespaces/config.ts new file mode 100644 index 0000000..21a1862 --- /dev/null +++ b/tests/e2e/test-cases/merged-namespaces/config.ts @@ -0,0 +1,9 @@ +import { TestCaseConfig } from '../test-case-config'; + +const config: TestCaseConfig = { + libraries: { + inlinedLibraries: ['extensions-package'], + }, +}; + +export = config; diff --git a/tests/e2e/test-cases/merged-namespaces/input.ts b/tests/e2e/test-cases/merged-namespaces/input.ts new file mode 100644 index 0000000..784726a --- /dev/null +++ b/tests/e2e/test-cases/merged-namespaces/input.ts @@ -0,0 +1,17 @@ +import { Ns1 as F1Ns1 } from './ns1'; +import { Ns1 as F2Ns1 } from './ns2'; + +export { + Ns1 as F1Ns1, + Ns2 as F1Ns2, +} from './ns1'; + +export { + Ns1 as F2Ns1, + Ns2 as F2Ns2, +} from './ns2'; + +export interface Int { + f1: F1Ns1.SubNs1.Interface1; + f2: F2Ns1.SubNs1.Interface1; +} diff --git a/tests/e2e/test-cases/merged-namespaces/ns1.ts b/tests/e2e/test-cases/merged-namespaces/ns1.ts new file mode 100644 index 0000000..1f05808 --- /dev/null +++ b/tests/e2e/test-cases/merged-namespaces/ns1.ts @@ -0,0 +1,33 @@ +export type FooBar = string; + +export namespace Ns1 { + export namespace SubNs1 { + export interface Interface1 { + field1: FooBar; + } + } +} + +export namespace Ns1 { + export namespace SubNs1 { + export interface Interface2 { + field1: FooBar; + } + } +} + +export module Ns2 { + export module SubNs1 { + export interface Interface1 { + field1: Ns1.SubNs1.Interface1; + } + } +} + +export module Ns2 { + export module SubNs1 { + export interface Interface2 { + field1: Ns1.SubNs1.Interface2; + } + } +} diff --git a/tests/e2e/test-cases/merged-namespaces/ns2.ts b/tests/e2e/test-cases/merged-namespaces/ns2.ts new file mode 100644 index 0000000..4940aaa --- /dev/null +++ b/tests/e2e/test-cases/merged-namespaces/ns2.ts @@ -0,0 +1,33 @@ +export type FooBar = number; + +export namespace Ns1 { + export namespace SubNs1 { + export interface Interface1 { + field1: FooBar; + } + } +} + +export namespace Ns1 { + export namespace SubNs1 { + export interface Interface2 { + field1: FooBar; + } + } +} + +export module Ns2 { + export module SubNs1 { + export interface Interface1 { + field1: Ns1.SubNs1.Interface1; + } + } +} + +export module Ns2 { + export module SubNs1 { + export interface Interface2 { + field1: Ns1.SubNs1.Interface2; + } + } +} diff --git a/tests/e2e/test-cases/merged-namespaces/output.d.ts b/tests/e2e/test-cases/merged-namespaces/output.d.ts new file mode 100644 index 0000000..9c6a65f --- /dev/null +++ b/tests/e2e/test-cases/merged-namespaces/output.d.ts @@ -0,0 +1,71 @@ +export type FooBar = string; +declare namespace Ns1 { + namespace SubNs1 { + interface Interface1 { + field1: FooBar; + } + } +} +declare namespace Ns1 { + namespace SubNs1 { + interface Interface2 { + field1: FooBar; + } + } +} +declare namespace Ns2 { + namespace SubNs1 { + interface Interface1 { + field1: Ns1.SubNs1.Interface1; + } + } +} +declare namespace Ns2 { + namespace SubNs1 { + interface Interface2 { + field1: Ns1.SubNs1.Interface2; + } + } +} +export type FooBar$1 = number; +declare namespace Ns1$1 { + namespace SubNs1 { + interface Interface1 { + field1: FooBar$1; + } + } +} +declare namespace Ns1$1 { + namespace SubNs1 { + interface Interface2 { + field1: FooBar$1; + } + } +} +declare namespace Ns2$1 { + namespace SubNs1 { + interface Interface1 { + field1: Ns1$1.SubNs1.Interface1; + } + } +} +declare namespace Ns2$1 { + namespace SubNs1 { + interface Interface2 { + field1: Ns1$1.SubNs1.Interface2; + } + } +} +export interface Int { + f1: Ns1.SubNs1.Interface1; + f2: Ns1$1.SubNs1.Interface1; +} + +export { + Ns1 as F1Ns1, + Ns1$1 as F2Ns1, + Ns2 as F1Ns2, + Ns2$1 as F2Ns2, +}; + +export {}; diff --git a/tests/e2e/test-cases/names-collision-across-files/config.ts b/tests/e2e/test-cases/names-collision-across-files/config.ts new file mode 100644 index 0000000..7f3c5de --- /dev/null +++ b/tests/e2e/test-cases/names-collision-across-files/config.ts @@ -0,0 +1,6 @@ +import { TestCaseConfig } from '../test-case-config'; + +const config: TestCaseConfig = { +}; + +export = config; diff --git a/tests/e2e/test-cases/names-collision-across-files/file1.ts b/tests/e2e/test-cases/names-collision-across-files/file1.ts new file mode 100644 index 0000000..c24afc7 --- /dev/null +++ b/tests/e2e/test-cases/names-collision-across-files/file1.ts @@ -0,0 +1,25 @@ +const TEMPLATE = 'template1'; +export default TEMPLATE; + +export const MergedSymbol = ''; +export interface MergedSymbol { + test(): void +}; + +export interface Interface { + field1: number; +} + +export function func(one: number) {} + +export type TypeName = Pick; + +export interface AnotherInterface { + field1: number; +} + +export function anotherFunc(one: NamespaceName.Local) {} + +export namespace NamespaceName { + export interface Local {} +} diff --git a/tests/e2e/test-cases/names-collision-across-files/file2.ts b/tests/e2e/test-cases/names-collision-across-files/file2.ts new file mode 100644 index 0000000..f042cb6 --- /dev/null +++ b/tests/e2e/test-cases/names-collision-across-files/file2.ts @@ -0,0 +1,25 @@ +const TEMPLATE = 'template2'; +export default TEMPLATE; + +export const MergedSymbol = ''; +export interface MergedSymbol { + test(): void +}; + +export interface Interface { + field2: number; +} + +export function func(two: number) {} + +export type TypeName = Pick; + +export interface AnotherInterface { + field2: number; +} + +export function anotherFunc(two: NamespaceName.Local) {} + +export namespace NamespaceName { + export interface Local {} +} diff --git a/tests/e2e/test-cases/names-collision-across-files/import-star-1.ts b/tests/e2e/test-cases/names-collision-across-files/import-star-1.ts new file mode 100644 index 0000000..7f44c7b --- /dev/null +++ b/tests/e2e/test-cases/names-collision-across-files/import-star-1.ts @@ -0,0 +1,17 @@ +import * as fakePackage from 'fake-package'; +import { Interface as FPI1, Interface } from 'fake-package'; + +import * as file1 from './file1'; +import * as file1ButDifferent from './file1'; +import { AnotherInterface as AiFromFile1 } from './file1'; +import * as file2 from './file2'; + +export interface Inter { + field: file1.Interface; + field2: file2.AnotherInterface; + field3: file1ButDifferent.TypeName; + field4: AiFromFile1; + field5: fakePackage.Interface; + field6: FPI1; + field7: Interface; +} diff --git a/tests/e2e/test-cases/names-collision-across-files/import-star-2.ts b/tests/e2e/test-cases/names-collision-across-files/import-star-2.ts new file mode 100644 index 0000000..e738a73 --- /dev/null +++ b/tests/e2e/test-cases/names-collision-across-files/import-star-2.ts @@ -0,0 +1,17 @@ +import * as fakePackageButDifferent from 'fake-package'; +import { Interface as FPI2, Interface } from 'fake-package'; + +import * as file1 from './file1'; +import * as file1ButDifferent from './file1'; +import { AnotherInterface as AiFromFile1 } from './file1'; +import * as file2 from './file2'; + +export interface Inter2 { + field: file1.Interface; + field2: file2.AnotherInterface; + field3: file1ButDifferent.TypeName; + field4: AiFromFile1; + field5: fakePackageButDifferent.Interface; + field6: FPI2; + field7: Interface; +} diff --git a/tests/e2e/test-cases/names-collision-across-files/input.ts b/tests/e2e/test-cases/names-collision-across-files/input.ts new file mode 100644 index 0000000..79fb173 --- /dev/null +++ b/tests/e2e/test-cases/names-collision-across-files/input.ts @@ -0,0 +1,29 @@ +export { + default as TEMPLATE1, + MergedSymbol as MS1, + Interface as I1, + TypeName as T1, + func as f1, + NamespaceName as NS1, + + // rename these to include them into import + AnotherInterface as AI1, + anotherFunc as af1, + +} from './file1'; + +export { + default as TEMPLATE2, + MergedSymbol as MS2, + Interface as I2, + TypeName as T2, + func as f2, + NamespaceName as NS2, + + // yes, keep these without renaming so we can check that these aren't exported with wrong names + AnotherInterface, + anotherFunc, +} from './file2'; + +export { Inter } from './import-star-1'; +export { Inter2 } from './import-star-2'; 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 new file mode 100644 index 0000000..89baf8e --- /dev/null +++ b/tests/e2e/test-cases/names-collision-across-files/output.d.ts @@ -0,0 +1,79 @@ +import * as fakePackage from 'fake-package'; +import * as fakePackageButDifferent from 'fake-package'; +import { Interface as FPI1, Interface as FPI2, Interface as Interface$2 } from 'fake-package'; + +declare const TEMPLATE = "template1"; +declare const MergedSymbol = ""; +interface MergedSymbol { + test(): void; +} +interface Interface { + field1: number; +} +declare function func(one: number): void; +type TypeName = Pick; +interface AnotherInterface { + field1: number; +} +declare function anotherFunc(one: NamespaceName.Local): void; +declare namespace NamespaceName { + interface Local { + } +} +declare const TEMPLATE$1 = "template2"; +declare const MergedSymbol$1 = ""; +interface MergedSymbol$1 { + test(): void; +} +interface Interface$1 { + field2: number; +} +declare function func$1(two: number): void; +type TypeName$1 = Pick; +interface AnotherInterface$1 { + field2: number; +} +declare function anotherFunc$1(two: NamespaceName$1.Local): void; +declare namespace NamespaceName$1 { + interface Local { + } +} +export interface Inter { + field: Interface; + field2: AnotherInterface$1; + field3: TypeName; + field4: AnotherInterface; + field5: fakePackage.Interface; + field6: FPI1; + field7: FPI1; +} +export interface Inter2 { + field: Interface; + field2: AnotherInterface$1; + field3: TypeName; + field4: AnotherInterface; + field5: fakePackageButDifferent.Interface; + field6: FPI2; + field7: FPI1; +} + +export { + AnotherInterface as AI1, + AnotherInterface$1 as AnotherInterface, + Interface as I1, + Interface$1 as I2, + MergedSymbol as MS1, + MergedSymbol$1 as MS2, + NamespaceName as NS1, + NamespaceName$1 as NS2, + TEMPLATE as TEMPLATE1, + TEMPLATE$1 as TEMPLATE2, + TypeName as T1, + TypeName$1 as T2, + anotherFunc as af1, + anotherFunc$1 as anotherFunc, + func as f1, + func$1 as f2, +}; + +export {}; diff --git a/tests/e2e/test-cases/re-export-as-default-of-unnamed-class/output.d.ts b/tests/e2e/test-cases/re-export-as-default-of-unnamed-class/output.d.ts index bfc878d..dd53fd0 100644 --- a/tests/e2e/test-cases/re-export-as-default-of-unnamed-class/output.d.ts +++ b/tests/e2e/test-cases/re-export-as-default-of-unnamed-class/output.d.ts @@ -1,4 +1,8 @@ -export default class { +declare class _default { } +export { + _default as default, +}; + export {}; diff --git a/tests/e2e/test-cases/rename-local-class/class-rename-1.ts b/tests/e2e/test-cases/rename-local-class/class-rename-1.ts new file mode 100644 index 0000000..ecba3d0 --- /dev/null +++ b/tests/e2e/test-cases/rename-local-class/class-rename-1.ts @@ -0,0 +1,3 @@ +import { OriginalClassName as LocalClassName1 } from './class'; + +export class ClassRename1 extends LocalClassName1 {} diff --git a/tests/e2e/test-cases/rename-local-class/class-rename-2.ts b/tests/e2e/test-cases/rename-local-class/class-rename-2.ts new file mode 100644 index 0000000..a3f4bb5 --- /dev/null +++ b/tests/e2e/test-cases/rename-local-class/class-rename-2.ts @@ -0,0 +1,3 @@ +import { OriginalClassName as LocalClassName2 } from './class'; + +export class ClassRename2 extends LocalClassName2 {} diff --git a/tests/e2e/test-cases/rename-local-class/class.ts b/tests/e2e/test-cases/rename-local-class/class.ts new file mode 100644 index 0000000..cd7c763 --- /dev/null +++ b/tests/e2e/test-cases/rename-local-class/class.ts @@ -0,0 +1 @@ +export class OriginalClassName {} diff --git a/tests/e2e/test-cases/rename-local-class/config.ts b/tests/e2e/test-cases/rename-local-class/config.ts new file mode 100644 index 0000000..f1fb1a8 --- /dev/null +++ b/tests/e2e/test-cases/rename-local-class/config.ts @@ -0,0 +1,5 @@ +import { TestCaseConfig } from '../../test-cases/test-case-config'; + +const config: TestCaseConfig = {}; + +export = config; diff --git a/tests/e2e/test-cases/rename-local-class/input.ts b/tests/e2e/test-cases/rename-local-class/input.ts new file mode 100644 index 0000000..db1a185 --- /dev/null +++ b/tests/e2e/test-cases/rename-local-class/input.ts @@ -0,0 +1,2 @@ +export { ClassRename1 } from './class-rename-1'; +export { ClassRename2 } from './class-rename-2'; diff --git a/tests/e2e/test-cases/rename-local-class/output.d.ts b/tests/e2e/test-cases/rename-local-class/output.d.ts new file mode 100644 index 0000000..a504d85 --- /dev/null +++ b/tests/e2e/test-cases/rename-local-class/output.d.ts @@ -0,0 +1,8 @@ +declare class OriginalClassName { +} +export declare class ClassRename1 extends OriginalClassName { +} +export declare class ClassRename2 extends OriginalClassName { +} + +export {};