@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/package.json +35 -0
- package/src/catalog.ts +880 -0
- package/src/check.ts +2495 -0
- package/src/index.ts +41 -0
- package/src/overlay.ts +158 -0
- package/src/source.ts +684 -0
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
|
+
};
|