Skip to content

Commit 432d977

Browse files
committed
Generalize CollectorEntity to support a given declaration being exported multiple times (fixes GitHub issue #950)
1 parent d0f872e commit 432d977

File tree

14 files changed

+200
-129
lines changed

14 files changed

+200
-129
lines changed

apps/api-extractor/.vscode/launch.json

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
"program": "${workspaceFolder}/lib/start.js",
1212
"cwd": "${workspaceFolder}/../../build-tests/api-extractor-test-01",
1313
"args": [
14-
"-d",
14+
"--debug",
1515
"run",
16-
"-l"
16+
"--local"
1717
],
1818
"sourceMaps": true
1919
},
@@ -24,9 +24,9 @@
2424
"program": "${workspaceFolder}/lib/start.js",
2525
"cwd": "${workspaceFolder}/../../build-tests/api-extractor-test-02",
2626
"args": [
27-
"-d",
27+
"--debug",
2828
"run",
29-
"-l"
29+
"--local"
3030
],
3131
"sourceMaps": true
3232
},
@@ -37,9 +37,9 @@
3737
"program": "${workspaceFolder}/lib/start.js",
3838
"cwd": "${workspaceFolder}/../../build-tests/api-extractor-test-03",
3939
"args": [
40-
"-d",
40+
"--debug",
4141
"run",
42-
"-l"
42+
"--local"
4343
],
4444
"sourceMaps": true
4545
},
@@ -50,9 +50,9 @@
5050
"program": "${workspaceFolder}/lib/start.js",
5151
"cwd": "${workspaceFolder}/../../build-tests/api-extractor-test-04",
5252
"args": [
53-
"-d",
53+
"--debug",
5454
"run",
55-
"-l"
55+
"--local"
5656
],
5757
"sourceMaps": true
5858
},
@@ -63,22 +63,24 @@
6363
"program": "${workspaceFolder}/lib/start.js",
6464
"cwd": "${workspaceFolder}/../../build-tests/api-extractor-test-05",
6565
"args": [
66-
"-d",
66+
"--debug",
6767
"run",
68-
"-l"
68+
"--local"
6969
],
7070
"sourceMaps": true
7171
},
7272
{
7373
"type": "node",
7474
"request": "launch",
75-
"name": "test-06",
75+
"name": "scenario",
7676
"program": "${workspaceFolder}/lib/start.js",
77-
"cwd": "${workspaceFolder}/../../build-tests/api-extractor-test-06",
77+
"cwd": "${workspaceFolder}/../../build-tests/api-extractor-scenarios",
7878
"args": [
79-
"-d",
79+
"--debug",
8080
"run",
81-
"-l"
81+
"--local",
82+
"--config",
83+
"./temp/configs/api-extractor-defaultExportOfEntryPoint1.json"
8284
],
8385
"sourceMaps": true
8486
},
@@ -89,9 +91,9 @@
8991
"program": "${workspaceFolder}/lib/start.js",
9092
"cwd": "(your project path)",
9193
"args": [
92-
"-d",
94+
"--debug",
9395
"run",
94-
"-l"
96+
"--local"
9597
],
9698
"sourceMaps": true
9799
}

apps/api-extractor/src/collector/Collector.ts

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -248,27 +248,17 @@ export class Collector {
248248
let entity: CollectorEntity | undefined = this._entitiesByAstSymbol.get(astSymbol);
249249

250250
if (!entity) {
251-
entity = new CollectorEntity({
252-
astSymbol: astSymbol,
253-
originalName: exportedName || astSymbol.localName,
254-
exported: !!exportedName
255-
});
251+
entity = new CollectorEntity(astSymbol);
256252

257253
this._entitiesByAstSymbol.set(astSymbol, entity);
258254
this._entitiesBySymbol.set(astSymbol.followedSymbol, entity);
259255
this._entities.push(entity);
260256

261257
this._collectReferenceDirectives(astSymbol);
262-
} else {
263-
if (exportedName) {
264-
if (!entity.exported) {
265-
throw new InternalError('CollectorEntity should have been marked as exported');
266-
}
267-
if (entity.originalName !== exportedName) {
268-
throw new InternalError(`The symbol ${exportedName} was also exported as ${entity.originalName};`
269-
+ ` this is not supported yet`);
270-
}
271-
}
258+
}
259+
260+
if (exportedName) {
261+
entity.addExportName(exportedName);
272262
}
273263
}
274264

@@ -294,31 +284,39 @@ export class Collector {
294284

295285
// First collect the explicit package exports (named)
296286
for (const entity of this._entities) {
297-
if (entity.exported && entity.originalName !== ts.InternalSymbolName.Default) {
298-
299-
if (usedNames.has(entity.originalName)) {
287+
for (const exportName of entity.exportNames) {
288+
if (usedNames.has(exportName)) {
300289
// This should be impossible
301-
throw new InternalError(`A package cannot have two exports with the name ${entity.originalName}`);
290+
throw new InternalError(`A package cannot have two exports with the name "${exportName}"`);
302291
}
303292

304-
entity.nameForEmit = entity.originalName;
305-
306-
usedNames.add(entity.nameForEmit);
293+
usedNames.add(exportName);
307294
}
308295
}
309296

310297
// Next generate unique names for the non-exports that will be emitted (and the default export)
311298
for (const entity of this._entities) {
312-
if (!entity.exported || entity.originalName === ts.InternalSymbolName.Default) {
313-
let suffix: number = 1;
314-
entity.nameForEmit = entity.astSymbol.localName;
315299

316-
while (usedNames.has(entity.nameForEmit)) {
317-
entity.nameForEmit = `${entity.astSymbol.localName}_${++suffix}`;
318-
}
300+
// If this entity is exported exactly once, then emit the exported name
301+
if (entity.singleExportName !== undefined && entity.singleExportName !== ts.InternalSymbolName.Default) {
302+
entity.nameForEmit = entity.singleExportName;
303+
continue;
304+
}
305+
306+
// If the localName happens to be the same as one of the exports, then emit that name
307+
if (entity.exportNames.has(entity.astSymbol.localName)) {
308+
entity.nameForEmit = entity.astSymbol.localName;
309+
continue;
310+
}
319311

320-
usedNames.add(entity.nameForEmit);
312+
// In all other cases, generate a unique name based on the localName
313+
let suffix: number = 1;
314+
let nameForEmit: string = entity.astSymbol.localName;
315+
while (usedNames.has(nameForEmit)) {
316+
nameForEmit = `${entity.astSymbol.localName}_${++suffix}`;
321317
}
318+
entity.nameForEmit = nameForEmit;
319+
usedNames.add(nameForEmit);
322320
}
323321
}
324322

apps/api-extractor/src/collector/CollectorEntity.ts

Lines changed: 74 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4+
import * as ts from 'typescript';
5+
46
import { AstSymbol } from '../analyzer/AstSymbol';
57
import { Collector } from './Collector';
8+
import { Sort } from '@microsoft/node-core-library';
69

710
/**
8-
* Constructor options for CollectorEntity
9-
*/
10-
export interface ICollectorEntityOptions {
11-
readonly astSymbol: AstSymbol;
12-
readonly originalName: string;
13-
readonly exported: boolean;
14-
}
15-
16-
/**
17-
* This is a data structure used by DtsRollupGenerator to track an AstSymbol that may be
18-
* emitted in the *.d.ts file.
11+
* This is a data structure used by the Collector to track an AstSymbol that may be emitted in the *.d.ts file.
12+
*
1913
* @remarks
2014
* The additional contextual state beyond AstSymbol is:
2115
* - Whether it's an export of this entry point or not
@@ -27,28 +21,22 @@ export class CollectorEntity {
2721
*/
2822
public readonly astSymbol: AstSymbol;
2923

30-
/**
31-
* The original name, prior to any renaming by DtsRollupGenerator._makeUniqueNames()
32-
*/
33-
public readonly originalName: string;
34-
35-
/**
36-
* Whether this API item is exported by the *.t.s file
37-
*/
38-
public readonly exported: boolean;
24+
private _exportNames: Set<string> = new Set<string>();
25+
private _exportNamesSorted: boolean = false;
26+
private _singleExportName: string | undefined = undefined;
3927

4028
private _nameForEmit: string | undefined = undefined;
4129

4230
private _sortKey: string | undefined = undefined;
4331

44-
public constructor(options: ICollectorEntityOptions) {
45-
this.astSymbol = options.astSymbol;
46-
this.originalName = options.originalName;
47-
this.exported = options.exported;
32+
public constructor(astSymbol: AstSymbol) {
33+
this.astSymbol = astSymbol;
4834
}
4935

5036
/**
51-
* The originalName, possibly renamed to ensure that all the top-level exports have unique names.
37+
* The declaration name that will be emitted in a .d.ts rollup. For non-exported declarations,
38+
* Collector._makeUniqueNames() may need to rename the declaration to avoid conflicts with other declarations
39+
* in that module.
5240
*/
5341
public get nameForEmit(): string | undefined {
5442
return this._nameForEmit;
@@ -59,13 +47,72 @@ export class CollectorEntity {
5947
this._sortKey = undefined; // invalidate the cached value
6048
}
6149

50+
/**
51+
* If this symbol is exported from the entry point, the list of export names.
52+
*
53+
* @remarks
54+
* Note that a given symbol may be exported more than once:
55+
* ```
56+
* class X { }
57+
* export { X }
58+
* export { X as Y }
59+
* ```
60+
*/
61+
public get exportNames(): ReadonlySet<string> {
62+
if (!this._exportNamesSorted) {
63+
Sort.sortSet(this._exportNames);
64+
this._exportNamesSorted = true;
65+
}
66+
return this._exportNames;
67+
}
68+
69+
/**
70+
* If exportNames contains only one string, then singleExportName is that string.
71+
* In all other cases, it is undefined.
72+
*/
73+
public get singleExportName(): string | undefined {
74+
return this._singleExportName;
75+
}
76+
77+
/**
78+
* This is true if exportNames contains only one string, and the declaration can be exported using the inline syntax
79+
* such as "export class X { }" instead of "export { X }".
80+
*/
81+
public get emitWithExportKeyword(): boolean {
82+
return this._singleExportName !== undefined
83+
&& this._singleExportName !== ts.InternalSymbolName.Default
84+
&& this.astSymbol.astImport === undefined;
85+
}
86+
87+
/**
88+
* Returns true if this symbol is an export for the entry point being analyzed.
89+
*/
90+
public get exported(): boolean {
91+
return this.exportNames.size > 0;
92+
}
93+
94+
/**
95+
* Adds a new exportName to the exportNames set.
96+
*/
97+
public addExportName(exportName: string): void {
98+
if (!this._exportNames.has(exportName)) {
99+
this._exportNamesSorted = false;
100+
this._exportNames.add(exportName);
101+
102+
if (this._exportNames.size === 1) {
103+
this._singleExportName = exportName;
104+
} else {
105+
this._singleExportName = undefined;
106+
}
107+
}
108+
}
109+
62110
/**
63111
* A sorting key used by DtsRollupGenerator._makeUniqueNames()
64112
*/
65113
public getSortKey(): string {
66114
if (!this._sortKey) {
67-
const name: string = this.nameForEmit || this.originalName;
68-
this._sortKey = Collector.getSortKeyIgnoringUnderscore(name);
115+
this._sortKey = Collector.getSortKeyIgnoringUnderscore(this.nameForEmit || this.astSymbol.localName);
69116
}
70117
return this._sortKey;
71118
}

0 commit comments

Comments
 (0)