UNPKG

15 kBPlain TextView Raw
1import * as tsm from 'ts-morph';
2import {
3 isClassDeclaration,
4 isEnumDeclaration,
5 isFunctionDeclaration,
6 isInterfaceDeclaration,
7 isNamespaceDeclaration,
8 isTypeAliasDeclaration,
9 isVariableDeclaration,
10} from '../types/declaration-type-guards';
11import { ModuleDeclarations } from '../types/module-declarations';
12import { log } from '../utils/log';
13import { isClass, newClass } from './classes';
14import { isEnum, newEnum } from './enums';
15import {
16 isExpression,
17 isVariableAssignmentExpression,
18 newExpression,
19 newVariableAssignmentExpression,
20} from './expression';
21import { isFileModule, newFileModule } from './file-modules';
22import {
23 isFunction,
24 isFunctionExpression,
25 newFunction,
26 newFunctionExpression,
27} from './functions';
28import { getDeclarationName } from './get-declaration-name';
29import { isInterface, newInterface } from './interfaces';
30import { isExportedDeclarations } from './is-exported-declarations';
31import { isGlobalDeclaration } from './is-global-declaration';
32import { isInternalDeclaration } from './is-internal-declaration';
33import { isNamespace, newNamespace } from './namespaces';
34import { sortByID } from './sort-by-id';
35import { SourceProvider } from './source-provider';
36import { toID } from './to-id';
37import { isTypeAlias, newTypeAlias } from './type-aliases';
38import { TypeChecker } from './type-checker';
39import { isVariable, newVariable } from './variables';
40
41type Module = tsm.SourceFile | tsm.ModuleDeclaration;
42
43interface ExportedDeclaration {
44 readonly exportID: string;
45 readonly exportName: string;
46 readonly declarationID: string;
47 readonly declarationName: string;
48 readonly declaration: tsm.ExportedDeclarations;
49}
50
51export function getPackageDeclarations({
52 project,
53 indexFile,
54 getSource,
55 getType,
56 maxDepth = 5,
57}: {
58 project: tsm.Project;
59 indexFile: tsm.SourceFile;
60 getSource: SourceProvider;
61 getType: TypeChecker;
62 maxDepth?: number;
63}): ModuleDeclarations {
64 return getModuleDeclarations({
65 module: indexFile,
66 moduleName: '',
67 maxDepth,
68 getSource,
69 getType,
70 project,
71 });
72}
73
74/**
75 * `getModuleDeclarations` extracts the public declarations from the given module.
76 *
77 * @param module - module (for example, a source file, node or namespace)
78 * @param maxDepth - maximum extraction depth for inner modules
79 * @param getSource - source provider
80 * @param getType - type checker
81 * @param moduleName - module's name, used to define IDs for declarations (optional)
82 */
83export function getModuleDeclarations({
84 module,
85 moduleName,
86 maxDepth,
87 getSource,
88 getType,
89 project,
90}: {
91 module: Module;
92 moduleName: string;
93 maxDepth: number;
94 getSource: SourceProvider;
95 getType: TypeChecker;
96 project?: tsm.Project;
97}): ModuleDeclarations {
98 log('getModuleDeclarations: extracting declarations: %O', {
99 moduleName,
100 maxDepth,
101 module,
102 });
103
104 const normalExportDeclarations = getNormalExportDeclarations({
105 module,
106 moduleName,
107 });
108 log('getModuleDeclarations: got normal export declarations: %O', {
109 moduleName,
110 total: normalExportDeclarations.length,
111 normalExportDeclarations,
112 });
113
114 const exportEqualsDeclarations = getExportEqualsDeclarations({
115 module,
116 moduleName,
117 });
118 log('getModuleDeclarations: got export equals declarations: %O', {
119 moduleName,
120 total: exportEqualsDeclarations.length,
121 exportEqualsDeclarations,
122 });
123
124 const ambientModulesDeclarations = getAmbientModulesDeclarations({
125 project,
126 });
127 log('getModuleDeclarations: got ambient modules declarations: %O', {
128 moduleName,
129 total: ambientModulesDeclarations.length,
130 ambientModulesDeclarations,
131 });
132
133 const globalAmbientDeclarations = getGlobalAmbientDeclarations({
134 module,
135 moduleName,
136 });
137 log('getModuleDeclarations: got global ambient declarations: %O', {
138 moduleName,
139 total: globalAmbientDeclarations.length,
140 globalAmbientDeclarations,
141 });
142
143 return extractModuleDeclarations({
144 exportedDeclarations: [
145 ...normalExportDeclarations,
146 ...exportEqualsDeclarations,
147 ...ambientModulesDeclarations,
148 ...globalAmbientDeclarations,
149 ],
150 maxDepth,
151 getSource,
152 getType,
153 });
154}
155
156function getNormalExportDeclarations({
157 module,
158 moduleName,
159}: {
160 module: Module;
161 moduleName: string;
162}): ExportedDeclaration[] {
163 const namedExports = new Set<string>();
164
165 return Array.from(module.getExportedDeclarations())
166 .flatMap(([exportName, declarations]) => {
167 return declarations.flatMap((declaration) => {
168 // Skip internal/private declarations
169 if (isInternalDeclaration({ declaration, name: exportName })) {
170 return [];
171 }
172
173 const exportID = toID(moduleName, exportName);
174 const declarationName = getDeclarationName({
175 exportName,
176 declaration,
177 });
178 const declarationID = toID(moduleName, declarationName);
179
180 // Keep track of named exports for the filter step
181 if (declarationID === exportID) {
182 namedExports.add(declarationID);
183 }
184
185 return {
186 exportID,
187 exportName,
188 declarationID,
189 declarationName,
190 declaration,
191 };
192 });
193 })
194 .filter(({ exportID, declarationID }) => {
195 // Keep only named exports or default exports
196 // with no corresponding named export
197 return (
198 declarationID === exportID || !namedExports.has(declarationID)
199 );
200 });
201}
202
203function getExportEqualsDeclarations({
204 module,
205 moduleName,
206}: {
207 module: Module;
208 moduleName: string;
209}): ExportedDeclaration[] {
210 // Skip shorthand ambient modules without body
211 // (for example, `declare module 'foo';`)
212 if (tsm.Node.isModuleDeclaration(module) && !module.hasBody()) {
213 return [];
214 }
215
216 const exportIdentifier = module
217 .getExportAssignment((ea) => ea.isExportEquals())
218 ?.getLastChildByKind(tsm.SyntaxKind.Identifier);
219 if (!exportIdentifier) {
220 return [];
221 }
222
223 const exportName = exportIdentifier.getText();
224
225 return exportIdentifier.getDefinitionNodes().flatMap((declaration) => {
226 // Skip internal or unsupported declarations
227 if (
228 isInternalDeclaration({ declaration, name: exportName }) ||
229 !isExportedDeclarations(declaration)
230 ) {
231 return [];
232 }
233
234 // Skip namespaces since `getNormalExportDeclarations` already extracts
235 // the inner declarations of an export equals namespace
236 // as non-namespaced declarations belonging to the parent module.
237 // See snapshot for `export-equals-function-and-namespace.test.ts`.
238 if (isNamespace(declaration)) {
239 return [];
240 }
241
242 const exportID = toID(moduleName, exportName);
243 const declarationName = getDeclarationName({
244 exportName,
245 declaration,
246 });
247 const declarationID = toID(moduleName, declarationName);
248
249 return {
250 exportID,
251 exportName,
252 declarationID,
253 declarationName,
254 declaration,
255 };
256 });
257}
258
259function getAmbientModulesDeclarations({
260 project,
261}: {
262 project?: tsm.Project;
263}): ExportedDeclaration[] {
264 if (!project) {
265 return [];
266 }
267
268 return project.getAmbientModules().flatMap((symbol) => {
269 return symbol.getDeclarations().flatMap((declaration) => {
270 const filepath = declaration.getSourceFile().getFilePath();
271 if (
272 !tsm.Node.isModuleDeclaration(declaration) ||
273 filepath.startsWith('/node_modules')
274 ) {
275 return [];
276 }
277
278 const exportName = declaration.getName();
279 const declarationName = exportName;
280
281 // Remove surrounding quotes and eventual spaces
282 const exportID = exportName
283 .replace(/"|'/g, '')
284 .replace(/\s/g, '_')
285 .trim();
286 const declarationID = exportID;
287
288 return {
289 exportID,
290 exportName,
291 declarationID,
292 declarationName,
293 declaration,
294 };
295 });
296 });
297}
298
299function getGlobalAmbientDeclarations({
300 module,
301 moduleName,
302}: {
303 module: Module;
304 moduleName: string;
305}): ExportedDeclaration[] {
306 if (!tsm.Node.isSourceFile(module)) {
307 return [];
308 }
309
310 // See https://www.typescriptlang.org/docs/handbook/declaration-files/by-example.html#global-variables
311 const globalCandidates = [
312 ...module.getVariableDeclarations(),
313 ...module.getFunctions(),
314 ...module.getModules(),
315 ];
316
317 return globalCandidates.flatMap((declaration) => {
318 // Global ambient functions must have a name
319 const exportName = declaration.getName()!;
320
321 if (
322 !isGlobalDeclaration({ declaration }) ||
323 isInternalDeclaration({ declaration, name: exportName })
324 ) {
325 return [];
326 }
327
328 const exportID = toID(moduleName, exportName);
329 const declarationName = getDeclarationName({
330 exportName,
331 declaration,
332 });
333 const declarationID = toID(moduleName, declarationName);
334
335 return {
336 exportID,
337 exportName,
338 declarationID,
339 declarationName,
340 declaration,
341 };
342 });
343}
344
345function extractModuleDeclarations({
346 exportedDeclarations,
347 maxDepth,
348 getSource,
349 getType,
350}: {
351 exportedDeclarations: ExportedDeclaration[];
352 maxDepth: number;
353 getSource: SourceProvider;
354 getType: TypeChecker;
355}): ModuleDeclarations {
356 const exportedFunctions = new Set<string>();
357 const exportedNamespaces = new Set<string>();
358
359 const declarations = exportedDeclarations
360 .flatMap(
361 ({
362 exportID,
363 declarationID: id,
364 declarationName: name,
365 declaration,
366 }) => {
367 if (isVariable(declaration)) {
368 return newVariable({ id, name, declaration, getSource });
369 }
370
371 if (isVariableAssignmentExpression(declaration)) {
372 return newVariableAssignmentExpression({
373 id,
374 name,
375 declaration,
376 getSource,
377 });
378 }
379
380 if (isExpression(declaration)) {
381 return newExpression({ id, name, declaration, getSource });
382 }
383
384 if (isFunction(declaration)) {
385 // Skip ambient function overloads
386 if (exportedFunctions.has(exportID)) {
387 return [];
388 }
389
390 exportedFunctions.add(exportID);
391 return newFunction({
392 id,
393 name,
394 declaration,
395 getSource,
396 getType,
397 });
398 }
399
400 if (isFunctionExpression(declaration)) {
401 return newFunctionExpression({
402 id,
403 name,
404 declaration,
405 getSource,
406 getType,
407 });
408 }
409
410 if (isClass(declaration)) {
411 return newClass({
412 id,
413 name,
414 declaration,
415 getSource,
416 getType,
417 });
418 }
419
420 if (isInterface(declaration)) {
421 return newInterface({
422 id,
423 name,
424 declaration,
425 getSource,
426 getType,
427 });
428 }
429
430 if (isEnum(declaration)) {
431 return newEnum({ id, name, declaration, getSource });
432 }
433
434 if (isTypeAlias(declaration)) {
435 return newTypeAlias({ id, name, declaration, getSource });
436 }
437
438 if (isNamespace(declaration) && maxDepth > 0) {
439 // Skip merged or nested namespace declarations
440 if (exportedNamespaces.has(exportID)) {
441 return [];
442 }
443
444 const declarations = getModuleDeclarations({
445 module: declaration,
446 moduleName: id,
447 maxDepth: maxDepth - 1,
448 getSource,
449 getType,
450 });
451
452 exportedNamespaces.add(exportID);
453 return newNamespace({
454 id,
455 name,
456 declaration,
457 declarations,
458 getSource,
459 });
460 }
461
462 // From `import * as ns from module; export { ns };`
463 // or from `export * as ns from module`.
464 if (isFileModule(declaration) && maxDepth > 0) {
465 const declarations = getModuleDeclarations({
466 module: declaration,
467 moduleName: id,
468 maxDepth: maxDepth - 1,
469 getSource,
470 getType,
471 });
472
473 return newFileModule({
474 id,
475 name,
476 declaration,
477 declarations,
478 getSource,
479 });
480 }
481
482 return [];
483 }
484 )
485 .sort(sortByID);
486
487 return {
488 variables: declarations.filter(isVariableDeclaration),
489 functions: declarations.filter(isFunctionDeclaration),
490 classes: declarations.filter(isClassDeclaration),
491 interfaces: declarations.filter(isInterfaceDeclaration),
492 enums: declarations.filter(isEnumDeclaration),
493 typeAliases: declarations.filter(isTypeAliasDeclaration),
494 namespaces: declarations.filter(isNamespaceDeclaration),
495 };
496}