Skip to content

Commit

Permalink
Merge pull request #248 from timocov/issue185
Browse files Browse the repository at this point in the history
  • Loading branch information
timocov committed Mar 30, 2023
2 parents a7ad68d + 14e9df0 commit 213998d
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 43 deletions.
66 changes: 49 additions & 17 deletions src/bundle-generator.ts
Expand Up @@ -148,6 +148,8 @@ 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}`);

Expand Down Expand Up @@ -179,11 +181,24 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
imports: new Map(),
statements: [],
renamedExports: [],
declarationsRenaming: new Map(),
};

const outputOptions: OutputOptions = entry.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');

return {
needStrip: defaultExport === undefined || defaultExport.originalName !== 'default' && statement.getSourceFile() !== rootSourceFile,
newName: isNodeNamedDeclaration(statement) ? collectionResult.declarationsRenaming.get(statement) : undefined,
};
};

const updateResultCommonParams = {
isStatementUsed: (statement: ts.Statement | ts.SourceFile) => isNodeUsed(
statement,
Expand Down Expand Up @@ -213,7 +228,31 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:

return getModuleInfo(fileNameOrModuleLike, criteria);
},
resolveIdentifier: (identifier: ts.Identifier) => resolveIdentifier(typeChecker, identifier),
resolveIdentifier: (identifier: ts.Identifier) => {
const resolvedDeclaration = resolveIdentifier(typeChecker, identifier);
if (resolvedDeclaration === undefined) {
return undefined;
}

const storedValue = collectionResult.declarationsRenaming.get(resolvedDeclaration);
if (storedValue !== undefined) {
return storedValue;
}

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);
}

return identifierName;
},
getDeclarationsForExportedAssignment: (exportAssignment: ts.ExportAssignment) => {
const symbolForExpression = typeChecker.getSymbolAtLocation(exportAssignment.expression);
if (symbolForExpression === undefined) {
Expand Down Expand Up @@ -314,27 +353,21 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
return generateOutput(
{
...collectionResult,
needStripDefaultKeywordForStatement: (statement: ts.Statement) => {
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');
return defaultExport === undefined || defaultExport.originalName !== 'default' && statement.getSourceFile() !== rootSourceFile;
},
needStripDefaultKeywordForStatement,
shouldStatementHasExportKeyword: (statement: ts.Statement) => {
const statementExports = getExportsForStatement(rootFileExports, typeChecker, statement);

// 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 hasStatementedDefaultKeyword = hasNodeModifier(statement, ts.SyntaxKind.DefaultKeyword);
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' && hasStatementedDefaultKeyword && statement.getSourceFile() === rootSourceFile;
const shouldBeDefaultExportedDirectly = exp.exportedName === 'default' && hasStatementDefaultKeyword && statement.getSourceFile() === rootSourceFile;
return shouldBeDefaultExportedDirectly || exp.exportedName === exp.originalName;
}) !== undefined;

Expand Down Expand Up @@ -402,6 +435,7 @@ interface CollectingResult {
imports: Map<string, ModuleImportsSet>;
statements: ts.Statement[];
renamedExports: string[];
declarationsRenaming: Map<ts.NamedDeclaration, string>;
}

type NodeWithReferencedModule = ts.ExportDeclaration | ts.ModuleDeclaration | ts.ImportTypeNode | ts.ImportEqualsDeclaration | ts.ImportDeclaration;
Expand All @@ -418,7 +452,7 @@ interface UpdateParams {
* 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'];
resolveIdentifier(identifier: ts.NamedDeclaration['name']): string | undefined;
getDeclarationsForExportedAssignment(exportAssignment: ts.ExportAssignment): ts.Declaration[];
getDeclarationUsagesSourceFiles(declaration: ts.NamedDeclaration): Set<ts.SourceFile | ts.ModuleDeclaration>;
areDeclarationSame(a: ts.NamedDeclaration, b: ts.NamedDeclaration): boolean;
Expand Down Expand Up @@ -512,25 +546,23 @@ function updateResultForRootSourceFile(params: UpdateParams, result: CollectingR
continue;
}

const exportedNameNode = params.resolveIdentifier(statement.expression);
if (exportedNameNode === undefined) {
const originalName = params.resolveIdentifier(statement.expression);
if (originalName === undefined) {
continue;
}

const originalName = exportedNameNode.getText();
result.renamedExports.push(`${originalName} as default`);
continue;
}

// 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 exportedNameNode = params.resolveIdentifier(exportItem.name);
if (exportedNameNode === undefined) {
const originalName = params.resolveIdentifier(exportItem.name);
if (originalName === undefined) {
continue;
}

const originalName = exportedNameNode.getText();
const exportedName = exportItem.name.getText();

if (originalName !== exportedName) {
Expand Down
29 changes: 20 additions & 9 deletions src/generate-output.ts
Expand Up @@ -17,9 +17,14 @@ export interface OutputParams extends OutputHelpers {
renamedExports: string[];
}

export interface NeedStripDefaultKeywordResult {
needStrip: boolean;
newName?: string;
}

export interface OutputHelpers {
shouldStatementHasExportKeyword(statement: ts.Statement): boolean;
needStripDefaultKeywordForStatement(statement: ts.Statement): boolean;
needStripDefaultKeywordForStatement(statement: ts.Statement): NeedStripDefaultKeywordResult;
needStripConstFromConstEnum(constEnum: ts.EnumDeclaration): boolean;
needStripImportFromImportTypeNode(importType: ts.ImportTypeNode): boolean;
}
Expand Down Expand Up @@ -119,7 +124,6 @@ function compareStatementText(a: StatementText, b: StatementText): number {

function getStatementText(statement: ts.Statement, includeSortingValue: boolean, helpers: OutputHelpers): StatementText {
const shouldStatementHasExportKeyword = helpers.shouldStatementHasExportKeyword(statement);
const needStripDefaultKeyword = helpers.needStripDefaultKeywordForStatement(statement);

const printer = ts.createPrinter(
{
Expand Down Expand Up @@ -152,13 +156,20 @@ function getStatementText(statement: ts.Statement, includeSortingValue: boolean,
modifiersMap[ts.SyntaxKind.ConstKeyword] = false;
}

let newName: string | undefined;

// strip the `default` keyword from node
if (modifiersMap[ts.SyntaxKind.DefaultKeyword] && needStripDefaultKeyword) {
// 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;
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;
}

newName = needStripDefaultKeywordResult.newName;
}
}

Expand All @@ -182,7 +193,7 @@ function getStatementText(statement: ts.Statement, includeSortingValue: boolean,
modifiersMap[ts.SyntaxKind.DeclareKeyword] = true;
}

return recreateRootLevelNodeWithModifiers(node, modifiersMap, shouldStatementHasExportKeyword);
return recreateRootLevelNodeWithModifiers(node, modifiersMap, newName, shouldStatementHasExportKeyword);
},
}
);
Expand Down
34 changes: 17 additions & 17 deletions src/helpers/typescript.ts
Expand Up @@ -194,13 +194,13 @@ export function getExportsForSourceFile(typeChecker: ts.TypeChecker, sourceFileS
exp.symbol = getActualSymbol(exp.symbol, typeChecker);

const resolvedIdentifier = resolveIdentifierBySymbol(exp.symbol);
exp.originalName = resolvedIdentifier !== undefined ? resolvedIdentifier.getText() : exp.symbol.escapedName as string;
exp.originalName = resolvedIdentifier?.name !== undefined ? resolvedIdentifier.name.getText() : exp.symbol.escapedName as string;
});

return result;
}

export function resolveIdentifier(typeChecker: ts.TypeChecker, identifier: ts.Identifier): ts.NamedDeclaration['name'] {
export function resolveIdentifier(typeChecker: ts.TypeChecker, identifier: ts.Identifier): ts.NamedDeclaration | undefined {
const symbol = getDeclarationNameSymbol(identifier, typeChecker);
if (symbol === null) {
return undefined;
Expand All @@ -209,7 +209,7 @@ export function resolveIdentifier(typeChecker: ts.TypeChecker, identifier: ts.Id
return resolveIdentifierBySymbol(symbol);
}

function resolveIdentifierBySymbol(identifierSymbol: ts.Symbol): ts.NamedDeclaration['name'] {
function resolveIdentifierBySymbol(identifierSymbol: ts.Symbol): ts.NamedDeclaration | undefined {
const declarations = getDeclarationsForSymbol(identifierSymbol);
if (declarations.length === 0) {
return undefined;
Expand All @@ -220,13 +220,13 @@ function resolveIdentifierBySymbol(identifierSymbol: ts.Symbol): ts.NamedDeclara
return undefined;
}

return decl.name;
return decl;
}

export function getExportsForStatement(
exportedSymbols: readonly SourceFileExport[],
typeChecker: ts.TypeChecker,
statement: ts.Statement
statement: ts.Statement | ts.NamedDeclaration
): SourceFileExport[] {
if (ts.isVariableStatement(statement)) {
if (statement.declarationList.declarations.length === 0) {
Expand Down Expand Up @@ -316,8 +316,8 @@ export function modifiersMapToArray(modifiersMap: ModifiersMap): ts.Modifier[] {
});
}

export function recreateRootLevelNodeWithModifiers(node: ts.Node, modifiersMap: ModifiersMap, keepComments: boolean = true): ts.Node {
const newNode = recreateRootLevelNodeWithModifiersImpl(node, modifiersMap);
export function recreateRootLevelNodeWithModifiers(node: ts.Node, modifiersMap: ModifiersMap, newName?: string, keepComments: boolean = true): ts.Node {
const newNode = recreateRootLevelNodeWithModifiersImpl(node, modifiersMap, newName);

if (keepComments) {
ts.setCommentRange(newNode, ts.getCommentRange(node));
Expand All @@ -327,7 +327,7 @@ export function recreateRootLevelNodeWithModifiers(node: ts.Node, modifiersMap:
}

// eslint-disable-next-line complexity
function recreateRootLevelNodeWithModifiersImpl(node: ts.Node, modifiersMap: ModifiersMap): ts.Node {
function recreateRootLevelNodeWithModifiersImpl(node: ts.Node, modifiersMap: ModifiersMap, newName?: string): ts.Node {
const modifiers = modifiersMapToArray(modifiersMap);

if (ts.isArrowFunction(node)) {
Expand All @@ -344,7 +344,7 @@ function recreateRootLevelNodeWithModifiersImpl(node: ts.Node, modifiersMap: Mod
if (ts.isClassDeclaration(node)) {
return ts.factory.createClassDeclaration(
modifiers,
node.name,
newName || node.name,
node.typeParameters,
node.heritageClauses,
node.members
Expand All @@ -354,7 +354,7 @@ function recreateRootLevelNodeWithModifiersImpl(node: ts.Node, modifiersMap: Mod
if (ts.isClassExpression(node)) {
return ts.factory.createClassExpression(
modifiers,
node.name,
newName || node.name,
node.typeParameters,
node.heritageClauses,
node.members
Expand All @@ -364,7 +364,7 @@ function recreateRootLevelNodeWithModifiersImpl(node: ts.Node, modifiersMap: Mod
if (ts.isEnumDeclaration(node)) {
return ts.factory.createEnumDeclaration(
modifiers,
node.name,
newName || node.name,
node.members
);
}
Expand All @@ -391,7 +391,7 @@ function recreateRootLevelNodeWithModifiersImpl(node: ts.Node, modifiersMap: Mod
return ts.factory.createFunctionDeclaration(
modifiers,
node.asteriskToken,
node.name,
newName || node.name,
node.typeParameters,
node.parameters,
node.type,
Expand All @@ -403,7 +403,7 @@ function recreateRootLevelNodeWithModifiersImpl(node: ts.Node, modifiersMap: Mod
return ts.factory.createFunctionExpression(
modifiers,
node.asteriskToken,
node.name,
newName || node.name,
node.typeParameters,
node.parameters,
node.type,
Expand All @@ -424,15 +424,15 @@ function recreateRootLevelNodeWithModifiersImpl(node: ts.Node, modifiersMap: Mod
return ts.factory.createImportEqualsDeclaration(
modifiers,
node.isTypeOnly,
node.name,
newName || node.name,
node.moduleReference
);
}

if (ts.isInterfaceDeclaration(node)) {
return ts.factory.createInterfaceDeclaration(
modifiers,
node.name,
newName || node.name,
node.typeParameters,
node.heritageClauses,
node.members
Expand All @@ -451,7 +451,7 @@ function recreateRootLevelNodeWithModifiersImpl(node: ts.Node, modifiersMap: Mod
if (ts.isTypeAliasDeclaration(node)) {
return ts.factory.createTypeAliasDeclaration(
modifiers,
node.name,
newName || node.name,
node.typeParameters,
node.type
);
Expand All @@ -470,7 +470,7 @@ If you're seeing this error, please report a bug on https://github.com/timocov/d

export function getModifiers(node: ts.Node): readonly ts.Modifier[] | undefined {
if (!ts.canHaveModifiers(node)) {
throw new Error(`Node kind=${ts.SyntaxKind[node.kind]} cannot have modifiers`);
return undefined;
}

return ts.getModifiers(node);
Expand Down
@@ -0,0 +1,3 @@
export default class {
second: number = 1;
}
@@ -0,0 +1 @@
export default function(second: number) {}
@@ -0,0 +1,3 @@
export default class {
first: number = 1;
}
@@ -0,0 +1,5 @@
import { TestCaseConfig } from '../../test-cases/test-case-config';

const config: TestCaseConfig = {};

export = config;
@@ -0,0 +1 @@
export default function(first: number) {}
@@ -0,0 +1,9 @@
export { default as myFunc1 } from './func';
export { default as myFunc2 } from './func';
export { default as myFunc3 } from './another-func';
export { default as myFunc4 } from './another-func';

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';
21 changes: 21 additions & 0 deletions tests/e2e/test-cases/export-default-unnamed-statement/output.d.ts
@@ -0,0 +1,21 @@
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 {
first: number;
}
declare class __DTS_BUNDLE_GENERATOR__GENERATED_NAME$4 {
second: number;
}

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,
};

export {};

0 comments on commit 213998d

Please sign in to comment.