Skip to content

Commit

Permalink
Fixed handling imports from inlined packages
Browse files Browse the repository at this point in the history
Fixes #199
  • Loading branch information
timocov committed Apr 26, 2022
1 parent 6ee68e0 commit 7be5535
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 27 deletions.
84 changes: 63 additions & 21 deletions src/bundle-generator.ts
Expand Up @@ -12,7 +12,6 @@ import {
getExportsForStatement,
hasNodeModifier,
isAmbientModule,
isDeclarationFromExternalModule,
isDeclareGlobalStatement,
isDeclareModule,
isNamespaceStatement,
Expand Down Expand Up @@ -192,12 +191,19 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
rootFileExportSymbols,
typesUsageEvaluator,
typeChecker,
program.isSourceFileDefaultLibrary.bind(program)
program.isSourceFileDefaultLibrary.bind(program),
criteria
);
},
shouldDeclareGlobalBeInlined: (currentModule: ModuleInfo) => Boolean(outputOptions.inlineDeclareGlobals) && currentModule.type === ModuleType.ShouldBeInlined,
shouldDeclareExternalModuleBeInlined: () => Boolean(outputOptions.inlineDeclareExternals),
getModuleInfo: (fileName: string) => getModuleInfo(fileName, criteria),
getModuleInfo: (fileNameOrModuleLike: string | ts.SourceFile | ts.ModuleDeclaration) => {
if (typeof fileNameOrModuleLike !== 'string') {
return getModuleLikeInfo(fileNameOrModuleLike, criteria);
}

return getModuleInfo(fileNameOrModuleLike, criteria);
},
resolveIdentifier: (identifier: ts.Identifier) => resolveIdentifier(typeChecker, identifier),
getDeclarationsForExportedAssignment: (exportAssignment: ts.ExportAssignment) => {
const symbolForExpression = typeChecker.getSymbolAtLocation(exportAssignment.expression);
Expand All @@ -209,7 +215,13 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
return getDeclarationsForSymbol(symbol);
},
getDeclarationUsagesSourceFiles: (declaration: ts.NamedDeclaration) => {
return getDeclarationUsagesSourceFiles(declaration, rootFileExportSymbols, typesUsageEvaluator, typeChecker);
return getDeclarationUsagesSourceFiles(
declaration,
rootFileExportSymbols,
typesUsageEvaluator,
typeChecker,
criteria
);
},
areDeclarationSame: (left: ts.NamedDeclaration, right: ts.NamedDeclaration) => {
const leftSymbols = splitTransientSymbol(getNodeSymbol(left, typeChecker) as ts.Symbol, typeChecker);
Expand Down Expand Up @@ -371,14 +383,14 @@ interface UpdateParams {
shouldStatementBeImported(statement: ts.DeclarationStatement): boolean;
shouldDeclareGlobalBeInlined(currentModule: ModuleInfo, statement: ts.ModuleDeclaration): boolean;
shouldDeclareExternalModuleBeInlined(): boolean;
getModuleInfo(fileName: string): ModuleInfo;
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']): ts.NamedDeclaration['name'];
getDeclarationsForExportedAssignment(exportAssignment: ts.ExportAssignment): ts.Declaration[];
getDeclarationUsagesSourceFiles(declaration: ts.NamedDeclaration): Set<ts.SourceFile>;
getDeclarationUsagesSourceFiles(declaration: ts.NamedDeclaration): Set<ts.SourceFile | ts.ModuleDeclaration>;
areDeclarationSame(a: ts.NamedDeclaration, b: ts.NamedDeclaration): boolean;
resolveReferencedModule(node: ts.ExportDeclaration): ts.SourceFile | ts.ModuleDeclaration | null;
}
Expand Down Expand Up @@ -449,11 +461,7 @@ function updateResultForRootSourceFile(params: UpdateParams, result: CollectingR
return false;
}

const fileName = ts.isSourceFile(resolvedModule)
? resolvedModule.fileName
: resolveModuleFileName(resolvedModule.getSourceFile().fileName, resolvedModule.name.text);

return params.getModuleInfo(fileName).type === ModuleType.ShouldBeImported;
return params.getModuleInfo(resolvedModule).type === ModuleType.ShouldBeImported;
}

updateResult(params, result);
Expand Down Expand Up @@ -578,17 +586,28 @@ function updateImportsForStatement(statement: ts.Statement | ts.SourceFile, para
}
}

function getClosestModuleLikeNode(node: ts.Node): ts.SourceFile | ts.ModuleDeclaration {
while (!ts.isModuleBlock(node) && !ts.isSourceFile(node)) {
node = node.parent;
}

// we need to find a module block and return its module declaration
// we don't need to handle empty modules/modules with jsdoc/etc
return ts.isSourceFile(node) ? node : node.parent;
}

function getDeclarationUsagesSourceFiles(
declaration: ts.NamedDeclaration,
rootFileExports: readonly ts.Symbol[],
typesUsageEvaluator: TypesUsageEvaluator,
typeChecker: ts.TypeChecker
): Set<ts.SourceFile> {
typeChecker: ts.TypeChecker,
criteria: ModuleCriteria
): Set<ts.SourceFile | ts.ModuleDeclaration> {
return new Set(
getExportedSymbolsUsingStatement(declaration, rootFileExports, typesUsageEvaluator, typeChecker)
getExportedSymbolsUsingStatement(declaration, rootFileExports, typesUsageEvaluator, typeChecker, criteria)
.map((symbol: ts.Symbol) => getDeclarationsForSymbol(symbol))
.reduce((acc: ts.Declaration[], val: ts.Declaration[]) => acc.concat(val), [])
.map((decl: ts.Declaration) => decl.getSourceFile())
.map(getClosestModuleLikeNode)
);
}

Expand Down Expand Up @@ -619,8 +638,12 @@ function addImport(statement: ts.DeclarationStatement, params: UpdateParams, imp
throw new Error(`Import/usage unnamed declaration: ${statement.getText()}`);
}

params.getDeclarationUsagesSourceFiles(statement).forEach((sourceFile: ts.SourceFile) => {
sourceFile.statements.forEach((st: ts.Statement) => {
params.getDeclarationUsagesSourceFiles(statement).forEach((sourceFile: ts.SourceFile | ts.ModuleDeclaration) => {
const statements = ts.isSourceFile(sourceFile)
? sourceFile.statements
: (sourceFile.body as ts.ModuleBlock).statements;

statements.forEach((st: ts.Statement) => {
if (!ts.isImportEqualsDeclaration(st) && !ts.isImportDeclaration(st)) {
return;
}
Expand Down Expand Up @@ -724,7 +747,8 @@ function shouldNodeBeImported(
rootFileExports: readonly ts.Symbol[],
typesUsageEvaluator: TypesUsageEvaluator,
typeChecker: ts.TypeChecker,
isDefaultLibrary: (sourceFile: ts.SourceFile) => boolean
isDefaultLibrary: (sourceFile: ts.SourceFile) => boolean,
criteria: ModuleCriteria
): boolean {
const nodeSymbol = getNodeSymbol(node, typeChecker);
if (nodeSymbol === null) {
Expand All @@ -751,14 +775,21 @@ function shouldNodeBeImported(
return false;
}

return getExportedSymbolsUsingStatement(node, rootFileExports, typesUsageEvaluator, typeChecker).length !== 0;
return getExportedSymbolsUsingStatement(
node,
rootFileExports,
typesUsageEvaluator,
typeChecker,
criteria
).length !== 0;
}

function getExportedSymbolsUsingStatement(
node: ts.NamedDeclaration,
rootFileExports: readonly ts.Symbol[],
typesUsageEvaluator: TypesUsageEvaluator,
typeChecker: ts.TypeChecker
typeChecker: ts.TypeChecker,
criteria: ModuleCriteria
): readonly ts.Symbol[] {
const nodeSymbol = getNodeSymbol(node, typeChecker);
if (nodeSymbol === null) {
Expand All @@ -773,7 +804,10 @@ function getExportedSymbolsUsingStatement(
// we should import only symbols which are used in types directly
return Array.from(symbolsUsingNode).filter((symbol: ts.Symbol) => {
const symbolsDeclarations = getDeclarationsForSymbol(symbol);
if (symbolsDeclarations.length === 0 || symbolsDeclarations.every(isDeclarationFromExternalModule)) {
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;
}

Expand All @@ -789,3 +823,11 @@ function getNodeSymbol(node: ts.Node, typeChecker: ts.TypeChecker): ts.Symbol |

return getDeclarationNameSymbol(nodeName, typeChecker);
}

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);
}
6 changes: 0 additions & 6 deletions src/helpers/typescript.ts
@@ -1,7 +1,5 @@
import * as ts from 'typescript';

import { getLibraryName } from './node-modules';

const namedDeclarationKinds = [
ts.SyntaxKind.InterfaceDeclaration,
ts.SyntaxKind.ClassDeclaration,
Expand Down Expand Up @@ -145,10 +143,6 @@ export function getDeclarationsForSymbol(symbol: ts.Symbol): ts.Declaration[] {
return result;
}

export function isDeclarationFromExternalModule(node: ts.Declaration): boolean {
return getLibraryName(node.getSourceFile().fileName) !== null;
}

export const enum ExportType {
CommonJS,
ES6Named,
Expand Down
9 changes: 9 additions & 0 deletions tests/e2e/test-cases/inline-from-deps-transitive/config.ts
@@ -0,0 +1,9 @@
import { TestCaseConfig } from '../test-case-config';

const config: TestCaseConfig = {
libraries: {
inlinedLibraries: ['fake-package', 'fake-fs'],
},
};

export = config;
2 changes: 2 additions & 0 deletions tests/e2e/test-cases/inline-from-deps-transitive/input.ts
@@ -0,0 +1,2 @@
export { InterfaceWithFields } from 'fake-package';
export { File } from 'fake-fs';
16 changes: 16 additions & 0 deletions tests/e2e/test-cases/inline-from-deps-transitive/output.d.ts
@@ -0,0 +1,16 @@
import { Path } from 'fake-path';
import { InterfaceFromTypesPackage } from 'fake-types-lib';

export interface Interface {
}
export type Type = number | string;
export interface InterfaceWithFields {
field: Type;
field2: Interface;
field3: InterfaceFromTypesPackage;
}
export interface File {
path: Path;
}

export {};
11 changes: 11 additions & 0 deletions tests/e2e/test-cases/node_modules/@types/fake-node/index.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions tests/e2e/test-cases/node_modules/fake-package/index.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions tests/e2e/test-cases/rename-imports/input.ts
Expand Up @@ -3,4 +3,9 @@ import { InterfaceWithFields as FakePackageInterface } from 'fake-package';
export const myVar: FakePackageInterface = {
field: 2,
field2: {},
field3: {
field: '',
field2: 0,
field3: 0,
},
};

0 comments on commit 7be5535

Please sign in to comment.