@ontrails/adapter-kit 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/source.ts ADDED
@@ -0,0 +1,684 @@
1
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+
4
+ export type AdapterSourceExportExpectation = 'type' | 'value';
5
+
6
+ export type AdapterSourceExportKind =
7
+ | 'interface-value'
8
+ | 'interface-value-erased'
9
+ | 'type'
10
+ | 'type-alias-value'
11
+ | 'type-alias-value-erased'
12
+ | 'type-value'
13
+ | 'type-value-erased'
14
+ | 'value';
15
+
16
+ interface StarExportSpecifier {
17
+ readonly specifier: string;
18
+ readonly typeOnly: boolean;
19
+ }
20
+
21
+ interface NamedExportSpecifier {
22
+ readonly identifier: string;
23
+ readonly specifier: string;
24
+ readonly typeOnly: boolean;
25
+ }
26
+
27
+ interface NamedImportSpecifier {
28
+ readonly identifier: string;
29
+ readonly specifier: string;
30
+ readonly typeOnly: boolean;
31
+ }
32
+
33
+ interface ExportListSpecifier {
34
+ readonly exported: string;
35
+ readonly local: string;
36
+ readonly typeOnly: boolean;
37
+ }
38
+
39
+ type ImportKindResolver = (
40
+ importSpecifier: NamedImportSpecifier
41
+ ) => AdapterSourceExportKind | undefined;
42
+
43
+ export const adapterSourceExportKindHasType = (
44
+ kind: AdapterSourceExportKind | undefined
45
+ ): boolean =>
46
+ kind === 'interface-value' ||
47
+ kind === 'interface-value-erased' ||
48
+ kind === 'type' ||
49
+ kind === 'type-alias-value' ||
50
+ kind === 'type-alias-value-erased' ||
51
+ kind === 'type-value' ||
52
+ kind === 'type-value-erased';
53
+
54
+ export const adapterSourceExportKindHasValue = (
55
+ kind: AdapterSourceExportKind | undefined
56
+ ): boolean =>
57
+ kind === 'interface-value' ||
58
+ kind === 'type-alias-value' ||
59
+ kind === 'value' ||
60
+ kind === 'type-value';
61
+
62
+ const eraseExportValue = (
63
+ kind: AdapterSourceExportKind | undefined
64
+ ): AdapterSourceExportKind | undefined => {
65
+ if (kind === 'interface-value' || kind === 'interface-value-erased') {
66
+ return 'interface-value-erased';
67
+ }
68
+ if (kind === 'type-alias-value' || kind === 'type-alias-value-erased') {
69
+ return 'type-alias-value-erased';
70
+ }
71
+ if (kind === 'type-value' || kind === 'type-value-erased') {
72
+ return 'type-value-erased';
73
+ }
74
+ return kind === 'type' ? 'type' : undefined;
75
+ };
76
+
77
+ const preferErasedExportKind = (
78
+ current: AdapterSourceExportKind | undefined,
79
+ candidate: AdapterSourceExportKind | undefined
80
+ ): AdapterSourceExportKind | undefined => {
81
+ const erasedCandidate = eraseExportValue(candidate);
82
+ if (current === 'interface-value-erased' || !erasedCandidate) {
83
+ return current;
84
+ }
85
+ if (erasedCandidate === 'interface-value-erased') {
86
+ return erasedCandidate;
87
+ }
88
+ if (current === 'type-alias-value-erased') {
89
+ return current;
90
+ }
91
+ if (erasedCandidate === 'type-alias-value-erased') {
92
+ return erasedCandidate;
93
+ }
94
+ if (current === 'type-value-erased' || !erasedCandidate) {
95
+ return current;
96
+ }
97
+ return erasedCandidate === 'type-value-erased'
98
+ ? erasedCandidate
99
+ : (current ?? erasedCandidate);
100
+ };
101
+
102
+ const combineExportKinds = (
103
+ left: AdapterSourceExportKind | undefined,
104
+ right: AdapterSourceExportKind | undefined
105
+ ): AdapterSourceExportKind | undefined => {
106
+ if (!left) {
107
+ return right;
108
+ }
109
+ if (!right) {
110
+ return left;
111
+ }
112
+
113
+ const hasType =
114
+ adapterSourceExportKindHasType(left) ||
115
+ adapterSourceExportKindHasType(right);
116
+ const hasValue =
117
+ adapterSourceExportKindHasValue(left) ||
118
+ adapterSourceExportKindHasValue(right);
119
+ if (!hasValue) {
120
+ return preferErasedExportKind(left, right);
121
+ }
122
+ if (!hasType) {
123
+ return 'value';
124
+ }
125
+ if (left.startsWith('interface') || right.startsWith('interface')) {
126
+ return 'interface-value';
127
+ }
128
+ if (left.startsWith('type-alias') || right.startsWith('type-alias')) {
129
+ return 'type-alias-value';
130
+ }
131
+ return 'type-value';
132
+ };
133
+
134
+ const normalizePath = (path: string): string => path.replaceAll('\\', '/');
135
+
136
+ const normalizeRealPath = (path: string): string => {
137
+ try {
138
+ return normalizePath(realpathSync(path));
139
+ } catch {
140
+ return normalizePath(resolve(path));
141
+ }
142
+ };
143
+
144
+ const escapeRegExp = (value: string): string =>
145
+ value.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&');
146
+
147
+ const localDeclarationKind = (
148
+ code: string,
149
+ identifier: string,
150
+ exportedOnly = false
151
+ ): AdapterSourceExportKind | undefined => {
152
+ const escapedIdentifier = escapeRegExp(identifier);
153
+ const statementPrefix = '(?:^|[;\\n\\r])\\s*';
154
+ const exportPrefix = exportedOnly
155
+ ? `${statementPrefix}export\\s+`
156
+ : `${statementPrefix}(?:export\\s+)?`;
157
+ const valueDeclarationPattern = new RegExp(
158
+ `${exportPrefix}(?!declare\\s+)(?:(?:async\\s+)?function|const|let|var)\\s+${escapedIdentifier}\\b`,
159
+ 'u'
160
+ );
161
+ const hasValueDeclaration = valueDeclarationPattern.test(code);
162
+
163
+ const typeValueDeclarationPattern = new RegExp(
164
+ `${exportPrefix}(?!declare\\s+)(?:abstract\\s+)?(?:class|enum)\\s+${escapedIdentifier}\\b`,
165
+ 'u'
166
+ );
167
+ if (typeValueDeclarationPattern.test(code)) {
168
+ return 'type-value';
169
+ }
170
+
171
+ const interfaceDeclarationPattern = new RegExp(
172
+ `${exportPrefix}(?:declare\\s+)?interface\\s+${escapedIdentifier}\\b`,
173
+ 'u'
174
+ );
175
+ const hasInterfaceDeclaration = interfaceDeclarationPattern.test(code);
176
+ const typeAliasDeclarationPattern = new RegExp(
177
+ `${exportPrefix}(?:declare\\s+)?type\\s+${escapedIdentifier}\\b`,
178
+ 'u'
179
+ );
180
+ const hasTypeAliasDeclaration = typeAliasDeclarationPattern.test(code);
181
+ const hasTypeDeclaration = hasInterfaceDeclaration || hasTypeAliasDeclaration;
182
+ if (hasValueDeclaration && hasInterfaceDeclaration) {
183
+ return 'interface-value';
184
+ }
185
+ if (hasValueDeclaration && hasTypeAliasDeclaration) {
186
+ return 'type-alias-value';
187
+ }
188
+ if (hasValueDeclaration && hasTypeDeclaration) {
189
+ return 'type-value';
190
+ }
191
+ if (hasValueDeclaration) {
192
+ return 'value';
193
+ }
194
+ return hasTypeDeclaration ? 'type' : undefined;
195
+ };
196
+
197
+ const maskDeadSourceText = (
198
+ source: string,
199
+ options: { strings: boolean }
200
+ ): string => {
201
+ const output = [...source];
202
+ let index = 0;
203
+
204
+ const maskRange = (start: number, end: number): void => {
205
+ for (let cursor = start; cursor < end; cursor += 1) {
206
+ if (output[cursor] !== '\n') {
207
+ output[cursor] = ' ';
208
+ }
209
+ }
210
+ };
211
+
212
+ const skipQuoted = (quote: '"' | "'" | '`'): void => {
213
+ const start = index;
214
+ index += 1;
215
+ while (index < source.length) {
216
+ if (source[index] === '\\') {
217
+ index += 2;
218
+ continue;
219
+ }
220
+ if (source[index] === quote) {
221
+ index += 1;
222
+ break;
223
+ }
224
+ index += 1;
225
+ }
226
+ if (options.strings) {
227
+ maskRange(start, index);
228
+ }
229
+ };
230
+
231
+ while (index < source.length) {
232
+ if (source.startsWith('//', index)) {
233
+ const end = source.indexOf('\n', index + 2);
234
+ const stop = end === -1 ? source.length : end;
235
+ maskRange(index, stop);
236
+ index = stop;
237
+ continue;
238
+ }
239
+ if (source.startsWith('/*', index)) {
240
+ const end = source.indexOf('*/', index + 2);
241
+ const stop = end === -1 ? source.length : end + 2;
242
+ maskRange(index, stop);
243
+ index = stop;
244
+ continue;
245
+ }
246
+ const char = source[index];
247
+ if (char === '"' || char === "'" || char === '`') {
248
+ skipQuoted(char);
249
+ continue;
250
+ }
251
+ index += 1;
252
+ }
253
+
254
+ return output.join('');
255
+ };
256
+
257
+ const defaultExportKind = (
258
+ source: string
259
+ ): AdapterSourceExportKind | undefined => {
260
+ const code = maskDeadSourceText(source, { strings: true });
261
+ if (/\bexport\s+default\s+(?:async\s+)?function\*?\b/u.test(code)) {
262
+ return 'value';
263
+ }
264
+ if (/\bexport\s+default\s+(?:abstract\s+)?(?:class|enum)\b/u.test(code)) {
265
+ return 'type-value';
266
+ }
267
+ if (/\bexport\s+default\s+(?:interface|type)\b/u.test(code)) {
268
+ return 'type';
269
+ }
270
+
271
+ const expressionIdentifier =
272
+ /\bexport\s+default\s+(?<identifier>[A-Za-z_$][\w$]*)\b/u.exec(code)
273
+ ?.groups?.['identifier'];
274
+ if (expressionIdentifier) {
275
+ return localDeclarationKind(code, expressionIdentifier) ?? 'value';
276
+ }
277
+
278
+ return /\bexport\s+default\b/u.test(code) ? 'value' : undefined;
279
+ };
280
+
281
+ const localDefaultImportSpecifier = (
282
+ code: string,
283
+ stringsMaskedCode: string,
284
+ identifier: string
285
+ ): NamedImportSpecifier | undefined => {
286
+ const pattern =
287
+ /\bimport\s+(?<importTypeOnly>type\s+)?(?<local>[A-Za-z_$][\w$]*)(?:\s*,\s*(?:\{[\s\S]*?\}|\*\s+as\s+[A-Za-z_$][\w$]*))?\s+from\s+['"](?<specifier>[^'"]+)['"]/gu;
288
+
289
+ for (const match of code.matchAll(pattern)) {
290
+ if (!stringsMaskedCode.startsWith('import', match.index ?? 0)) {
291
+ continue;
292
+ }
293
+ if (match.groups?.['local'] !== identifier) {
294
+ continue;
295
+ }
296
+
297
+ return {
298
+ identifier: 'default',
299
+ specifier: match.groups?.['specifier'] ?? '',
300
+ typeOnly: Boolean(match.groups?.['importTypeOnly']),
301
+ };
302
+ }
303
+
304
+ return undefined;
305
+ };
306
+
307
+ const parseNamedImportSpecifier = (
308
+ item: string,
309
+ declarationTypeOnly: boolean,
310
+ specifier: string,
311
+ identifier: string
312
+ ): NamedImportSpecifier | undefined => {
313
+ const trimmedItem = item.trim();
314
+ if (!trimmedItem) {
315
+ return undefined;
316
+ }
317
+
318
+ const itemTypeOnly = declarationTypeOnly || trimmedItem.startsWith('type ');
319
+ const specifierText = trimmedItem.replace(/^type\s+/u, '');
320
+ const imported =
321
+ /^(?<imported>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<local>[A-Za-z_$][\w$]*))?$/u.exec(
322
+ specifierText
323
+ )?.groups;
324
+ const local = imported?.['local'] ?? imported?.['imported'];
325
+ const importedName = imported?.['imported'];
326
+ if (local !== identifier || !importedName) {
327
+ return undefined;
328
+ }
329
+
330
+ return {
331
+ identifier: importedName,
332
+ specifier,
333
+ typeOnly: itemTypeOnly,
334
+ };
335
+ };
336
+
337
+ const localNamedImportSpecifier = (
338
+ source: string,
339
+ identifier: string
340
+ ): NamedImportSpecifier | undefined => {
341
+ const code = maskDeadSourceText(source, { strings: false });
342
+ const stringsMaskedCode = maskDeadSourceText(source, { strings: true });
343
+ const defaultSpecifier = localDefaultImportSpecifier(
344
+ code,
345
+ stringsMaskedCode,
346
+ identifier
347
+ );
348
+ if (defaultSpecifier) {
349
+ return defaultSpecifier;
350
+ }
351
+
352
+ const pattern =
353
+ /\bimport\s+(?<importTypeOnly>type\s+)?\{(?<imports>[\s\S]*?)\}\s+from\s+['"](?<specifier>[^'"]+)['"]/gu;
354
+
355
+ for (const match of code.matchAll(pattern)) {
356
+ if (!stringsMaskedCode.startsWith('import', match.index ?? 0)) {
357
+ continue;
358
+ }
359
+
360
+ const declarationTypeOnly = Boolean(match.groups?.['importTypeOnly']);
361
+ const namedImports = match.groups?.['imports'] ?? '';
362
+ const specifier = match.groups?.['specifier'] ?? '';
363
+ for (const item of namedImports.split(',')) {
364
+ const namedSpecifier = parseNamedImportSpecifier(
365
+ item,
366
+ declarationTypeOnly,
367
+ specifier,
368
+ identifier
369
+ );
370
+ if (namedSpecifier) {
371
+ return namedSpecifier;
372
+ }
373
+ }
374
+ }
375
+
376
+ return undefined;
377
+ };
378
+
379
+ const parseExportListSpecifier = (
380
+ item: string,
381
+ declarationTypeOnly: boolean
382
+ ): ExportListSpecifier | undefined => {
383
+ const trimmedItem = item.trim();
384
+ if (!trimmedItem) {
385
+ return undefined;
386
+ }
387
+
388
+ const typeOnly = declarationTypeOnly || trimmedItem.startsWith('type ');
389
+ const specifierText = trimmedItem.replace(/^type\s+/u, '');
390
+ const exported =
391
+ /^(?<local>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<name>[A-Za-z_$][\w$]*))?$/u.exec(
392
+ specifierText
393
+ )?.groups;
394
+ const local = exported?.['local'];
395
+ if (!local) {
396
+ return undefined;
397
+ }
398
+
399
+ return {
400
+ exported: exported?.['name'] ?? local,
401
+ local,
402
+ typeOnly,
403
+ };
404
+ };
405
+
406
+ const exportedLocalBindingKind = (
407
+ source: string,
408
+ code: string,
409
+ local: string,
410
+ resolveImportKind: ImportKindResolver
411
+ ): AdapterSourceExportKind | undefined => {
412
+ const localKind = localDeclarationKind(code, local);
413
+ if (localKind) {
414
+ return localKind;
415
+ }
416
+
417
+ const importSpecifier = localNamedImportSpecifier(source, local);
418
+ if (!importSpecifier) {
419
+ return undefined;
420
+ }
421
+ const importedKind = resolveImportKind(importSpecifier);
422
+ return importSpecifier.typeOnly
423
+ ? eraseExportValue(importedKind)
424
+ : importedKind;
425
+ };
426
+
427
+ const namedExportKind = (
428
+ source: string,
429
+ identifier: string,
430
+ resolveImportKind: ImportKindResolver
431
+ ): AdapterSourceExportKind | undefined => {
432
+ if (identifier === 'default') {
433
+ const defaultKind = defaultExportKind(source);
434
+ if (defaultKind) {
435
+ return defaultKind;
436
+ }
437
+ }
438
+
439
+ const stringsMaskedCode = maskDeadSourceText(source, { strings: true });
440
+ const directKind = localDeclarationKind(stringsMaskedCode, identifier, true);
441
+ if (directKind) {
442
+ return directKind;
443
+ }
444
+
445
+ const exportCode = maskDeadSourceText(source, { strings: false });
446
+ const exportListPattern =
447
+ /\bexport\s+(?<typeOnly>type\s+)?\{(?<exports>[\s\S]*?)\}(?<from>\s+from\s+['"][^'"]+['"])?/gu;
448
+ let resolvedKind: AdapterSourceExportKind | undefined;
449
+
450
+ for (const match of exportCode.matchAll(exportListPattern)) {
451
+ if (!stringsMaskedCode.startsWith('export', match.index ?? 0)) {
452
+ continue;
453
+ }
454
+ if (match.groups?.['from']) {
455
+ continue;
456
+ }
457
+
458
+ const declarationTypeOnly = Boolean(match.groups?.['typeOnly']);
459
+ const namedExports = match.groups?.['exports'] ?? '';
460
+ for (const item of namedExports.split(',')) {
461
+ const exported = parseExportListSpecifier(item, declarationTypeOnly);
462
+ if (exported?.exported !== identifier) {
463
+ continue;
464
+ }
465
+
466
+ const localKind = exportedLocalBindingKind(
467
+ source,
468
+ stringsMaskedCode,
469
+ exported.local,
470
+ resolveImportKind
471
+ );
472
+ if (!localKind) {
473
+ continue;
474
+ }
475
+ resolvedKind = combineExportKinds(
476
+ resolvedKind,
477
+ exported.typeOnly ? eraseExportValue(localKind) : localKind
478
+ );
479
+ }
480
+ }
481
+
482
+ return resolvedKind;
483
+ };
484
+
485
+ const starExportSpecifiers = (
486
+ source: string
487
+ ): readonly StarExportSpecifier[] => {
488
+ const code = maskDeadSourceText(source, { strings: false });
489
+ const stringsMaskedCode = maskDeadSourceText(source, { strings: true });
490
+ return [
491
+ ...code.matchAll(
492
+ /\bexport\s+(?<typeOnly>type\s+)?\*\s+from\s+['"](?<specifier>[^'"]+)['"]/gu
493
+ ),
494
+ ]
495
+ .filter((match) => stringsMaskedCode.startsWith('export', match.index ?? 0))
496
+ .map((match) => ({
497
+ specifier: match.groups?.['specifier'] ?? '',
498
+ typeOnly: Boolean(match.groups?.['typeOnly']),
499
+ }));
500
+ };
501
+
502
+ const namedExportSpecifiers = (
503
+ source: string,
504
+ identifier: string
505
+ ): readonly NamedExportSpecifier[] => {
506
+ const code = maskDeadSourceText(source, { strings: false });
507
+ const stringsMaskedCode = maskDeadSourceText(source, { strings: true });
508
+ const exports: NamedExportSpecifier[] = [];
509
+ const pattern =
510
+ /\bexport\s+(?<typeOnly>type\s+)?\{(?<exports>[\s\S]*?)\}\s+from\s+['"](?<specifier>[^'"]+)['"]/gu;
511
+
512
+ for (const match of code.matchAll(pattern)) {
513
+ if (!stringsMaskedCode.startsWith('export', match.index ?? 0)) {
514
+ continue;
515
+ }
516
+
517
+ const specifier = match.groups?.['specifier'];
518
+ if (!specifier?.startsWith('.')) {
519
+ continue;
520
+ }
521
+
522
+ const declarationTypeOnly = Boolean(match.groups?.['typeOnly']);
523
+ const namedExports = match.groups?.['exports'] ?? '';
524
+ for (const item of namedExports.split(',')) {
525
+ const trimmedItem = item.trim();
526
+ if (!trimmedItem) {
527
+ continue;
528
+ }
529
+
530
+ const itemTypeOnly =
531
+ declarationTypeOnly || trimmedItem.startsWith('type ');
532
+ const specifierText = trimmedItem.replace(/^type\s+/u, '');
533
+ const exported =
534
+ /^(?<local>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<name>[A-Za-z_$][\w$]*))?$/u.exec(
535
+ specifierText
536
+ )?.groups;
537
+ const local = exported?.['local'];
538
+ if (!local || (exported?.['name'] ?? local) !== identifier) {
539
+ continue;
540
+ }
541
+
542
+ exports.push({
543
+ identifier: local,
544
+ specifier,
545
+ typeOnly: itemTypeOnly,
546
+ });
547
+ }
548
+ }
549
+
550
+ return exports;
551
+ };
552
+
553
+ const resolveLocalModuleSpecifier = (
554
+ sourcePath: string,
555
+ specifier: string
556
+ ): string | undefined => {
557
+ if (!specifier.startsWith('.')) {
558
+ return undefined;
559
+ }
560
+
561
+ const basePath = resolve(dirname(sourcePath), specifier);
562
+ const candidates = [
563
+ basePath,
564
+ basePath.endsWith('.js') ? `${basePath.slice(0, -3)}.ts` : undefined,
565
+ basePath.endsWith('.js') ? `${basePath.slice(0, -3)}.tsx` : undefined,
566
+ basePath.endsWith('.mjs') ? `${basePath.slice(0, -4)}.mts` : undefined,
567
+ `${basePath}.ts`,
568
+ `${basePath}.tsx`,
569
+ join(basePath, 'index.ts'),
570
+ join(basePath, 'index.tsx'),
571
+ ].filter((candidate): candidate is string => candidate !== undefined);
572
+
573
+ return candidates.find((candidate) => existsSync(candidate));
574
+ };
575
+
576
+ const resolveAdapterSourceExportKind = (
577
+ sourcePath: string,
578
+ identifier: string,
579
+ visited = new Set<string>()
580
+ ): AdapterSourceExportKind | undefined => {
581
+ const normalizedSourcePath = normalizeRealPath(sourcePath);
582
+ const visitKey = `${normalizedSourcePath}:${identifier}`;
583
+ if (visited.has(visitKey)) {
584
+ return undefined;
585
+ }
586
+ visited.add(visitKey);
587
+
588
+ let source: string;
589
+ try {
590
+ source = readFileSync(normalizedSourcePath, 'utf8');
591
+ } catch {
592
+ return undefined;
593
+ }
594
+
595
+ const directKind = namedExportKind(source, identifier, (importSpecifier) => {
596
+ const importTarget = resolveLocalModuleSpecifier(
597
+ normalizedSourcePath,
598
+ importSpecifier.specifier
599
+ );
600
+ if (!importTarget) {
601
+ return;
602
+ }
603
+ return resolveAdapterSourceExportKind(
604
+ importTarget,
605
+ importSpecifier.identifier,
606
+ new Set(visited)
607
+ );
608
+ });
609
+ let resolvedKind = directKind;
610
+ for (const exportSpecifier of namedExportSpecifiers(source, identifier)) {
611
+ const exportTarget = resolveLocalModuleSpecifier(
612
+ normalizedSourcePath,
613
+ exportSpecifier.specifier
614
+ );
615
+ if (!exportTarget) {
616
+ continue;
617
+ }
618
+
619
+ const reexportedKind = resolveAdapterSourceExportKind(
620
+ exportTarget,
621
+ exportSpecifier.identifier,
622
+ new Set(visited)
623
+ );
624
+ resolvedKind = combineExportKinds(
625
+ resolvedKind,
626
+ exportSpecifier.typeOnly
627
+ ? eraseExportValue(reexportedKind)
628
+ : reexportedKind
629
+ );
630
+ }
631
+
632
+ if (resolvedKind !== undefined) {
633
+ return resolvedKind;
634
+ }
635
+
636
+ for (const exportSpecifier of starExportSpecifiers(source)) {
637
+ const exportTarget = resolveLocalModuleSpecifier(
638
+ normalizedSourcePath,
639
+ exportSpecifier.specifier
640
+ );
641
+ if (!exportTarget) {
642
+ continue;
643
+ }
644
+
645
+ const reexportedKind = resolveAdapterSourceExportKind(
646
+ exportTarget,
647
+ identifier,
648
+ new Set(visited)
649
+ );
650
+ resolvedKind = combineExportKinds(
651
+ resolvedKind,
652
+ exportSpecifier.typeOnly
653
+ ? eraseExportValue(reexportedKind)
654
+ : reexportedKind
655
+ );
656
+ }
657
+
658
+ return resolvedKind;
659
+ };
660
+
661
+ /**
662
+ * Inspect a local TypeScript source path for the kind of named export it
663
+ * provides, following relative imports, named re-exports, and star re-exports.
664
+ */
665
+ export const adapterSourceExportKind = (
666
+ sourcePath: string,
667
+ identifier: string
668
+ ): AdapterSourceExportKind | undefined =>
669
+ resolveAdapterSourceExportKind(sourcePath, identifier);
670
+
671
+ /**
672
+ * Check whether a local TypeScript source path exports a named binding in the
673
+ * expected type or runtime value position.
674
+ */
675
+ export const adapterSourceExports = (
676
+ sourcePath: string,
677
+ identifier: string,
678
+ expected: AdapterSourceExportExpectation
679
+ ): boolean => {
680
+ const exportKind = adapterSourceExportKind(sourcePath, identifier);
681
+ return expected === 'type'
682
+ ? adapterSourceExportKindHasType(exportKind)
683
+ : adapterSourceExportKindHasValue(exportKind);
684
+ };