@ontrails/adapter-kit 1.0.0-beta.19

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/check.ts ADDED
@@ -0,0 +1,2064 @@
1
+ /**
2
+ * Shared adapter readiness checks for Warden and local author tooling.
3
+ *
4
+ * The engine reads package manifests and source files. It does not import
5
+ * runtime adapter packages, and runtime adapters must not import it.
6
+ */
7
+
8
+ import {
9
+ existsSync,
10
+ readdirSync,
11
+ readFileSync,
12
+ realpathSync,
13
+ statSync,
14
+ } from 'node:fs';
15
+ import { dirname, join, relative, resolve } from 'node:path';
16
+
17
+ import { deriveAdapterTargetCatalog } from './catalog.js';
18
+ import type {
19
+ AdapterTargetCatalog,
20
+ AdapterTargetCatalogDiagnosticCode,
21
+ AdapterTargetCatalogEntry,
22
+ AdapterTargetPlacement,
23
+ } from './catalog.js';
24
+
25
+ export type AdapterCheckDiagnosticCode =
26
+ | AdapterTargetCatalogDiagnosticCode
27
+ | 'dependency-direction'
28
+ | 'invalid-adapter-metadata'
29
+ | 'missing-conformance'
30
+ | 'missing-owner-conformance'
31
+ | 'missing-package-export'
32
+ | 'tooling-boundary'
33
+ | 'unknown-adapter-target'
34
+ | 'unsupported-placement';
35
+
36
+ export type AdapterCheckDiagnosticSeverity = 'error' | 'warn';
37
+
38
+ export interface AdapterCheckDiagnostic {
39
+ readonly code: AdapterCheckDiagnosticCode;
40
+ readonly message: string;
41
+ readonly packageJsonPath: string;
42
+ readonly packageName?: string | undefined;
43
+ readonly placement?: AdapterTargetPlacement | undefined;
44
+ readonly severity: AdapterCheckDiagnosticSeverity;
45
+ readonly target?: string | undefined;
46
+ }
47
+
48
+ export interface AdapterCheckSubject {
49
+ readonly conformanceTestPaths: readonly string[];
50
+ readonly key: string;
51
+ readonly ownerPackage: string;
52
+ readonly packageJsonPath: string;
53
+ readonly packageName: string;
54
+ readonly packageRoot: string;
55
+ readonly placement: AdapterTargetPlacement;
56
+ readonly target: string;
57
+ readonly targetKey: string;
58
+ readonly testingImport?: string | undefined;
59
+ }
60
+
61
+ export interface AdapterCheckReport {
62
+ readonly diagnostics: readonly AdapterCheckDiagnostic[];
63
+ readonly subjects: readonly AdapterCheckSubject[];
64
+ readonly targets: readonly AdapterTargetCatalogEntry[];
65
+ }
66
+
67
+ interface RootManifest {
68
+ readonly workspaces?: unknown;
69
+ }
70
+
71
+ interface AdapterCheckPackageManifest {
72
+ readonly dependencies?: unknown;
73
+ readonly devDependencies?: unknown;
74
+ readonly exports?: unknown;
75
+ readonly name?: unknown;
76
+ readonly optionalDependencies?: unknown;
77
+ readonly peerDependencies?: unknown;
78
+ readonly trails?: unknown;
79
+ }
80
+
81
+ interface WorkspacePackage {
82
+ readonly manifest: AdapterCheckPackageManifest;
83
+ readonly packageJsonPath: string;
84
+ readonly packageRoot: string;
85
+ readonly workspacePath: string;
86
+ }
87
+
88
+ interface AdapterMetadata {
89
+ readonly target: string;
90
+ }
91
+
92
+ const adapterKitPackageName = '@ontrails/adapter-kit';
93
+
94
+ const targetIdPattern = /^[a-z][a-z0-9-]*$/u;
95
+
96
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
97
+ Boolean(value && typeof value === 'object' && !Array.isArray(value));
98
+
99
+ const normalizePath = (path: string): string => path.replaceAll('\\', '/');
100
+
101
+ const normalizeRealPath = (path: string): string => {
102
+ try {
103
+ return normalizePath(realpathSync(path));
104
+ } catch {
105
+ return normalizePath(resolve(path));
106
+ }
107
+ };
108
+
109
+ const readJson = <T>(path: string): T | undefined => {
110
+ try {
111
+ return JSON.parse(readFileSync(path, 'utf8')) as T;
112
+ } catch {
113
+ return undefined;
114
+ }
115
+ };
116
+
117
+ const workspacePatternsFromManifest = (
118
+ manifest: RootManifest | undefined
119
+ ): readonly string[] => {
120
+ const { workspaces } = manifest ?? {};
121
+ if (Array.isArray(workspaces)) {
122
+ return workspaces.filter(
123
+ (pattern): pattern is string => typeof pattern === 'string'
124
+ );
125
+ }
126
+
127
+ const packages = isRecord(workspaces) ? workspaces['packages'] : undefined;
128
+ return Array.isArray(packages)
129
+ ? packages.filter(
130
+ (pattern): pattern is string => typeof pattern === 'string'
131
+ )
132
+ : [];
133
+ };
134
+
135
+ const workspaceDirsForPattern = (
136
+ rootDir: string,
137
+ pattern: string
138
+ ): readonly string[] => {
139
+ if (!pattern.endsWith('/*')) {
140
+ const workspaceDir = join(rootDir, pattern);
141
+ return existsSync(workspaceDir) ? [workspaceDir] : [];
142
+ }
143
+
144
+ const groupDir = join(rootDir, pattern.slice(0, -2));
145
+ if (!existsSync(groupDir)) {
146
+ return [];
147
+ }
148
+
149
+ return readdirSync(groupDir, { withFileTypes: true })
150
+ .filter((entry) => entry.isDirectory())
151
+ .map((entry) => join(groupDir, entry.name))
152
+ .toSorted();
153
+ };
154
+
155
+ const workspacePackages = (rootDir: string): readonly WorkspacePackage[] => {
156
+ const normalizedRoot = normalizeRealPath(rootDir);
157
+ const rootManifest = readJson<RootManifest>(
158
+ join(normalizedRoot, 'package.json')
159
+ );
160
+ const packages: WorkspacePackage[] = [];
161
+
162
+ for (const pattern of workspacePatternsFromManifest(rootManifest)) {
163
+ for (const workspaceDir of workspaceDirsForPattern(
164
+ normalizedRoot,
165
+ pattern
166
+ )) {
167
+ const packageJsonPath = join(workspaceDir, 'package.json');
168
+ const manifest = readJson<AdapterCheckPackageManifest>(packageJsonPath);
169
+ if (!manifest || typeof manifest.name !== 'string') {
170
+ continue;
171
+ }
172
+
173
+ const packageRoot = normalizeRealPath(dirname(packageJsonPath));
174
+ packages.push({
175
+ manifest,
176
+ packageJsonPath: normalizeRealPath(packageJsonPath),
177
+ packageRoot,
178
+ workspacePath: normalizePath(relative(normalizedRoot, packageRoot)),
179
+ });
180
+ }
181
+ }
182
+
183
+ return packages.toSorted((left, right) =>
184
+ left.workspacePath.localeCompare(right.workspacePath)
185
+ );
186
+ };
187
+
188
+ const resolveExportTarget = (
189
+ target: unknown,
190
+ depth = 0
191
+ ): string | undefined => {
192
+ if (typeof target === 'string') {
193
+ return target;
194
+ }
195
+ if (!isRecord(target) || depth > 8) {
196
+ return undefined;
197
+ }
198
+
199
+ for (const condition of ['bun', 'import', 'default', 'require'] as const) {
200
+ const resolvedTarget = resolveExportTarget(target[condition], depth + 1);
201
+ if (resolvedTarget) {
202
+ return resolvedTarget;
203
+ }
204
+ }
205
+ return undefined;
206
+ };
207
+
208
+ const isPackageRelativePath = (path: string): boolean =>
209
+ path.startsWith('./') && !normalizePath(path).includes('/../');
210
+
211
+ const exportTargetIsFile = (packageRoot: string, target: string): boolean => {
212
+ if (!isPackageRelativePath(target)) {
213
+ return false;
214
+ }
215
+
216
+ try {
217
+ return statSync(resolve(packageRoot, target)).isFile();
218
+ } catch {
219
+ return false;
220
+ }
221
+ };
222
+
223
+ const hasResolvableExport = (
224
+ workspace: WorkspacePackage,
225
+ key: string
226
+ ): boolean => {
227
+ const { exports: exportsValue } = workspace.manifest;
228
+ if (typeof exportsValue === 'string') {
229
+ return (
230
+ key === '.' && exportTargetIsFile(workspace.packageRoot, exportsValue)
231
+ );
232
+ }
233
+
234
+ if (!isRecord(exportsValue)) {
235
+ return false;
236
+ }
237
+
238
+ if (!Object.hasOwn(exportsValue, key)) {
239
+ if (key !== '.') {
240
+ return false;
241
+ }
242
+
243
+ const rootTarget = resolveExportTarget(exportsValue);
244
+ return (
245
+ rootTarget !== undefined &&
246
+ exportTargetIsFile(workspace.packageRoot, rootTarget)
247
+ );
248
+ }
249
+
250
+ const target = resolveExportTarget(exportsValue[key]);
251
+ return (
252
+ target !== undefined && exportTargetIsFile(workspace.packageRoot, target)
253
+ );
254
+ };
255
+
256
+ const dependencyMap = (value: unknown): Readonly<Record<string, unknown>> =>
257
+ isRecord(value) ? value : {};
258
+
259
+ const runtimeDependencyNames = (
260
+ manifest: AdapterCheckPackageManifest
261
+ ): ReadonlySet<string> =>
262
+ new Set([
263
+ ...Object.keys(dependencyMap(manifest.dependencies)),
264
+ ...Object.keys(dependencyMap(manifest.optionalDependencies)),
265
+ ...Object.keys(dependencyMap(manifest.peerDependencies)),
266
+ ]);
267
+
268
+ const trailAdapterMetadata = (
269
+ manifest: AdapterCheckPackageManifest
270
+ ): AdapterMetadata | undefined | null => {
271
+ const trails = isRecord(manifest.trails) ? manifest.trails : undefined;
272
+ const adapter = trails?.['adapter'];
273
+ if (adapter === undefined) {
274
+ return undefined;
275
+ }
276
+ if (!isRecord(adapter)) {
277
+ return null;
278
+ }
279
+
280
+ const { target } = adapter;
281
+ return typeof target === 'string' && targetIdPattern.test(target)
282
+ ? { target }
283
+ : null;
284
+ };
285
+
286
+ const placementForWorkspace = (
287
+ workspacePath: string
288
+ ): AdapterTargetPlacement | undefined =>
289
+ workspacePath.startsWith('adapters/') ? 'extracted' : undefined;
290
+
291
+ const diagnostic = (
292
+ packageJsonPath: string,
293
+ packageName: string | undefined,
294
+ code: AdapterCheckDiagnosticCode,
295
+ message: string,
296
+ target?: string,
297
+ placement?: AdapterTargetPlacement
298
+ ): AdapterCheckDiagnostic => ({
299
+ code,
300
+ message,
301
+ packageJsonPath,
302
+ ...(packageName === undefined ? {} : { packageName }),
303
+ ...(placement === undefined ? {} : { placement }),
304
+ severity: 'error',
305
+ ...(target === undefined ? {} : { target }),
306
+ });
307
+
308
+ const catalogDiagnostics = (
309
+ catalog: AdapterTargetCatalog
310
+ ): readonly AdapterCheckDiagnostic[] =>
311
+ catalog.diagnostics.map((entry) =>
312
+ diagnostic(
313
+ entry.packageJsonPath,
314
+ entry.packageName,
315
+ entry.code,
316
+ entry.message,
317
+ entry.target
318
+ )
319
+ );
320
+
321
+ const targetEntriesByTarget = (
322
+ targets: readonly AdapterTargetCatalogEntry[]
323
+ ): ReadonlyMap<string, AdapterTargetCatalogEntry> =>
324
+ new Map(targets.map((target) => [target.target, target]));
325
+
326
+ const collectSourceFiles = (dir: string): readonly string[] => {
327
+ if (!existsSync(dir)) {
328
+ return [];
329
+ }
330
+
331
+ const files: string[] = [];
332
+ const visit = (current: string): void => {
333
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
334
+ const child = join(current, entry.name);
335
+ if (entry.isDirectory()) {
336
+ visit(child);
337
+ continue;
338
+ }
339
+ if (entry.isFile() && child.endsWith('.ts')) {
340
+ files.push(normalizeRealPath(child));
341
+ }
342
+ }
343
+ };
344
+
345
+ visit(dir);
346
+ return files.toSorted();
347
+ };
348
+
349
+ const escapeRegExp = (value: string): string =>
350
+ value.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&');
351
+
352
+ const isIdentifierChar = (char: string | undefined): boolean =>
353
+ char !== undefined && /[$\w]/u.test(char);
354
+
355
+ const skipWhitespace = (source: string, start: number): number => {
356
+ let index = start;
357
+ while (/\s/u.test(source[index] ?? '')) {
358
+ index += 1;
359
+ }
360
+ return index;
361
+ };
362
+
363
+ const previousNonWhitespaceChar = (
364
+ source: string,
365
+ start: number
366
+ ): string | undefined => {
367
+ let index = start - 1;
368
+ while (index >= 0) {
369
+ const char = source[index];
370
+ if (!/\s/u.test(char ?? '')) {
371
+ return char;
372
+ }
373
+ index -= 1;
374
+ }
375
+ return undefined;
376
+ };
377
+
378
+ const regexLiteralCanStartAfter = (char: string | undefined): boolean =>
379
+ char === undefined || /[([{:;,=!?&|+\-*%^~<>]/u.test(char);
380
+
381
+ const regexLiteralCanStartAfterKeyword = (
382
+ source: string,
383
+ start: number
384
+ ): boolean =>
385
+ /\b(?:await|case|delete|do|else|in|instanceof|of|return|throw|typeof|void|yield)\s*$/u.test(
386
+ source.slice(0, start).trimEnd()
387
+ );
388
+
389
+ const startsRegexLiteral = (source: string, start: number): boolean => {
390
+ if (
391
+ source[start] !== '/' ||
392
+ source.startsWith('//', start) ||
393
+ source.startsWith('/*', start)
394
+ ) {
395
+ return false;
396
+ }
397
+
398
+ const previousChar = previousNonWhitespaceChar(source, start);
399
+ return (
400
+ regexLiteralCanStartAfter(previousChar) ||
401
+ regexLiteralCanStartAfterKeyword(source, start)
402
+ );
403
+ };
404
+
405
+ const skipRegexLiteral = (source: string, start: number): number => {
406
+ let index = start + 1;
407
+ let inCharacterClass = false;
408
+ while (index < source.length) {
409
+ const char = source[index];
410
+ if (char === '\\') {
411
+ index += 2;
412
+ continue;
413
+ }
414
+ if (char === '[') {
415
+ inCharacterClass = true;
416
+ index += 1;
417
+ continue;
418
+ }
419
+ if (char === ']' && inCharacterClass) {
420
+ inCharacterClass = false;
421
+ index += 1;
422
+ continue;
423
+ }
424
+ if (char === '/' && !inCharacterClass) {
425
+ index += 1;
426
+ while (/[a-z]/iu.test(source[index] ?? '')) {
427
+ index += 1;
428
+ }
429
+ return index;
430
+ }
431
+ index += 1;
432
+ }
433
+ return source.length;
434
+ };
435
+
436
+ const readQuotedString = (
437
+ source: string,
438
+ start: number
439
+ ): { readonly end: number; readonly value: string } | undefined => {
440
+ const quote = source[start];
441
+ if (quote !== '"' && quote !== "'") {
442
+ return undefined;
443
+ }
444
+
445
+ let index = start + 1;
446
+ let value = '';
447
+ while (index < source.length) {
448
+ const char = source[index];
449
+ if (char === '\\') {
450
+ value += source[index + 1] ?? '';
451
+ index += 2;
452
+ continue;
453
+ }
454
+ if (char === quote) {
455
+ return { end: index + 1, value };
456
+ }
457
+ value += char;
458
+ index += 1;
459
+ }
460
+
461
+ return undefined;
462
+ };
463
+
464
+ const skipBlockComment = (source: string, start: number): number => {
465
+ const end = source.indexOf('*/', start + 2);
466
+ return end === -1 ? source.length : end + 2;
467
+ };
468
+
469
+ const skipLineComment = (source: string, start: number): number => {
470
+ const end = source.indexOf('\n', start + 2);
471
+ return end === -1 ? source.length : end + 1;
472
+ };
473
+
474
+ const skipTrivia = (source: string, start: number): number => {
475
+ let index = skipWhitespace(source, start);
476
+ while (index < source.length) {
477
+ if (source.startsWith('//', index)) {
478
+ index = skipWhitespace(source, skipLineComment(source, index));
479
+ continue;
480
+ }
481
+ if (source.startsWith('/*', index)) {
482
+ index = skipWhitespace(source, skipBlockComment(source, index));
483
+ continue;
484
+ }
485
+ return index;
486
+ }
487
+ return index;
488
+ };
489
+
490
+ const skipQuotedLiteral = (source: string, start: number): number => {
491
+ const quote = source[start];
492
+ let index = start + 1;
493
+ while (index < source.length) {
494
+ if (source[index] === '\\') {
495
+ index += 2;
496
+ continue;
497
+ }
498
+ if (source[index] === quote) {
499
+ return index + 1;
500
+ }
501
+ index += 1;
502
+ }
503
+ return source.length;
504
+ };
505
+
506
+ const skipImportScanIgnoredToken = (
507
+ source: string,
508
+ start: number
509
+ ): number | undefined => {
510
+ if (source.startsWith('//', start)) {
511
+ return skipLineComment(source, start);
512
+ }
513
+ if (source.startsWith('/*', start)) {
514
+ return skipBlockComment(source, start);
515
+ }
516
+
517
+ const char = source[start];
518
+ if (char === '"' || char === "'" || char === '`') {
519
+ return skipQuotedLiteral(source, start);
520
+ }
521
+ if (char === '/' && startsRegexLiteral(source, start)) {
522
+ return skipRegexLiteral(source, start);
523
+ }
524
+
525
+ return undefined;
526
+ };
527
+
528
+ const importClauseHasRuntimeBinding = (
529
+ clause: string,
530
+ options: { readonly emptyNamedCounts?: boolean } = {}
531
+ ): boolean => {
532
+ const emptyNamedCounts = options.emptyNamedCounts ?? true;
533
+ const uncommented = clause
534
+ .replaceAll(/\/\*[\s\S]*?\*\//g, ' ')
535
+ .replaceAll(/\/\/[^\n\r]*/g, ' ');
536
+ const trimmed = uncommented.trim();
537
+ if (!trimmed) {
538
+ return false;
539
+ }
540
+
541
+ const namedStart = trimmed.indexOf('{');
542
+ if (namedStart === -1) {
543
+ return true;
544
+ }
545
+
546
+ if (trimmed.slice(0, namedStart).replaceAll(',', '').trim()) {
547
+ return true;
548
+ }
549
+
550
+ const namedEnd = trimmed.indexOf('}', namedStart + 1);
551
+ if (namedEnd === -1) {
552
+ return emptyNamedCounts;
553
+ }
554
+
555
+ const namedBindings = trimmed
556
+ .slice(namedStart + 1, namedEnd)
557
+ .split(',')
558
+ .map((binding) => binding.trim())
559
+ .filter(Boolean);
560
+ if (namedBindings.length === 0) {
561
+ return emptyNamedCounts;
562
+ }
563
+
564
+ return namedBindings.some((binding) => !/^type(?:\s|$)/u.test(binding));
565
+ };
566
+
567
+ const previousStatementStart = (source: string, start: number): number => {
568
+ let index = start - 1;
569
+ while (index >= 0) {
570
+ if (source[index] === ';' || source[index] === '}') {
571
+ return index + 1;
572
+ }
573
+ index -= 1;
574
+ }
575
+ return 0;
576
+ };
577
+
578
+ const importAppearsInTypePosition = (
579
+ source: string,
580
+ start: number
581
+ ): boolean => {
582
+ const prefix = source
583
+ .slice(previousStatementStart(source, start), start)
584
+ .replaceAll(/\/\*[\s\S]*?\*\//g, ' ')
585
+ .replaceAll(/\/\/[^\n\r]*/g, ' ')
586
+ .trimStart();
587
+ const lastLineStart =
588
+ Math.max(prefix.lastIndexOf('\n'), prefix.lastIndexOf('\r')) + 1;
589
+ const lastLine = prefix.slice(lastLineStart).trimStart();
590
+ if (
591
+ /^(?:export\s+)?(?:type|interface)\b/u.test(prefix) &&
592
+ lastLineStart > 0 &&
593
+ /^(?:const|let|var|using|await|return|throw|void|yield|new)\b/u.test(
594
+ lastLine
595
+ )
596
+ ) {
597
+ return false;
598
+ }
599
+
600
+ if (/^(?:export\s+)?(?:type|interface)\b/u.test(prefix)) {
601
+ return true;
602
+ }
603
+
604
+ const trimmed = prefix.trimEnd();
605
+ const lastAssignment = prefix.lastIndexOf('=');
606
+ const colonLooksLikeTypeAnnotation = (colonIndex: number): boolean => {
607
+ const objectLiteralStart = prefix.indexOf('{', lastAssignment + 1);
608
+ if (objectLiteralStart !== -1 && objectLiteralStart < colonIndex) {
609
+ return false;
610
+ }
611
+
612
+ const blockStart = prefix.lastIndexOf('{');
613
+ if (blockStart > colonIndex) {
614
+ const blockTail = prefix.slice(blockStart + 1).trimStart();
615
+ if (
616
+ /^(?:const|let|var|using|await|return|throw|void|yield|new)\b/u.test(
617
+ blockTail
618
+ )
619
+ ) {
620
+ return false;
621
+ }
622
+ }
623
+
624
+ const questionIndex = prefix.indexOf('?', lastAssignment + 1);
625
+ return questionIndex === -1 || questionIndex > colonIndex;
626
+ };
627
+
628
+ if (trimmed.endsWith('<') && isIdentifierChar(trimmed.at(-2))) {
629
+ return true;
630
+ }
631
+
632
+ if (trimmed.endsWith(':')) {
633
+ const trailingColon = prefix.lastIndexOf(':');
634
+ return colonLooksLikeTypeAnnotation(trailingColon);
635
+ }
636
+
637
+ if (/\b(?:as|extends|implements|satisfies|typeof)\s*$/u.test(trimmed)) {
638
+ return true;
639
+ }
640
+
641
+ const annotationColon = prefix.indexOf(':', lastAssignment + 1);
642
+ if (annotationColon === -1) {
643
+ return false;
644
+ }
645
+
646
+ return colonLooksLikeTypeAnnotation(annotationColon);
647
+ };
648
+
649
+ const importDeclarationSpecifier = (
650
+ source: string,
651
+ start: number
652
+ ): string | undefined => {
653
+ let index = skipTrivia(source, start + 'import'.length);
654
+ if (source[index] === '.') {
655
+ return undefined;
656
+ }
657
+ if (
658
+ source.startsWith('type', index) &&
659
+ !isIdentifierChar(source[index + 'type'.length])
660
+ ) {
661
+ return undefined;
662
+ }
663
+ const sideEffect = readQuotedString(source, index);
664
+ if (sideEffect) {
665
+ return sideEffect.value;
666
+ }
667
+
668
+ if (source[index] === '(') {
669
+ if (importAppearsInTypePosition(source, start)) {
670
+ return undefined;
671
+ }
672
+ index = skipTrivia(source, index + 1);
673
+ return readQuotedString(source, index)?.value;
674
+ }
675
+
676
+ const clauseStart = index;
677
+ while (index < source.length) {
678
+ if (source.startsWith('//', index)) {
679
+ index = skipLineComment(source, index);
680
+ continue;
681
+ }
682
+ if (source.startsWith('/*', index)) {
683
+ index = skipBlockComment(source, index);
684
+ continue;
685
+ }
686
+ const char = source[index];
687
+ if (char === '"' || char === "'" || char === '`') {
688
+ index = skipQuotedLiteral(source, index);
689
+ continue;
690
+ }
691
+ if (
692
+ source.startsWith('from', index) &&
693
+ !isIdentifierChar(source[index - 1]) &&
694
+ !isIdentifierChar(source[index + 'from'.length])
695
+ ) {
696
+ if (!importClauseHasRuntimeBinding(source.slice(clauseStart, index))) {
697
+ return undefined;
698
+ }
699
+ index = skipTrivia(source, index + 'from'.length);
700
+ return readQuotedString(source, index)?.value;
701
+ }
702
+ if (char === ';') {
703
+ return undefined;
704
+ }
705
+ index += 1;
706
+ }
707
+
708
+ return undefined;
709
+ };
710
+
711
+ const boundImportDeclarationSpecifier = (
712
+ source: string,
713
+ start: number
714
+ ): string | undefined => {
715
+ let index = skipTrivia(source, start + 'import'.length);
716
+ if (source[index] === '.') {
717
+ return undefined;
718
+ }
719
+ if (
720
+ source.startsWith('type', index) &&
721
+ !isIdentifierChar(source[index + 'type'.length])
722
+ ) {
723
+ return undefined;
724
+ }
725
+ if (readQuotedString(source, index)) {
726
+ return undefined;
727
+ }
728
+
729
+ if (source[index] === '(') {
730
+ if (importAppearsInTypePosition(source, start)) {
731
+ return undefined;
732
+ }
733
+ index = skipTrivia(source, index + 1);
734
+ return readQuotedString(source, index)?.value;
735
+ }
736
+
737
+ const clauseStart = index;
738
+ while (index < source.length) {
739
+ if (source.startsWith('//', index)) {
740
+ index = skipLineComment(source, index);
741
+ continue;
742
+ }
743
+ if (source.startsWith('/*', index)) {
744
+ index = skipBlockComment(source, index);
745
+ continue;
746
+ }
747
+ const char = source[index];
748
+ if (char === '"' || char === "'" || char === '`') {
749
+ index = skipQuotedLiteral(source, index);
750
+ continue;
751
+ }
752
+ if (
753
+ source.startsWith('from', index) &&
754
+ !isIdentifierChar(source[index - 1]) &&
755
+ !isIdentifierChar(source[index + 'from'.length])
756
+ ) {
757
+ if (
758
+ !importClauseHasRuntimeBinding(source.slice(clauseStart, index), {
759
+ emptyNamedCounts: false,
760
+ })
761
+ ) {
762
+ return undefined;
763
+ }
764
+ index = skipTrivia(source, index + 'from'.length);
765
+ return readQuotedString(source, index)?.value;
766
+ }
767
+ if (char === ';') {
768
+ return undefined;
769
+ }
770
+ index += 1;
771
+ }
772
+
773
+ return undefined;
774
+ };
775
+
776
+ const reExportDeclarationSpecifier = (
777
+ source: string,
778
+ start: number
779
+ ): string | undefined => {
780
+ let index = skipTrivia(source, start + 'export'.length);
781
+ if (
782
+ source.startsWith('type', index) &&
783
+ !isIdentifierChar(source[index + 'type'.length])
784
+ ) {
785
+ return undefined;
786
+ }
787
+
788
+ const clauseStart = index;
789
+ while (index < source.length) {
790
+ if (source.startsWith('//', index)) {
791
+ index = skipLineComment(source, index);
792
+ continue;
793
+ }
794
+ if (source.startsWith('/*', index)) {
795
+ index = skipBlockComment(source, index);
796
+ continue;
797
+ }
798
+ const char = source[index];
799
+ if (char === '"' || char === "'" || char === '`') {
800
+ index = skipQuotedLiteral(source, index);
801
+ continue;
802
+ }
803
+ if (char === '}') {
804
+ const next = skipTrivia(source, index + 1);
805
+ if (
806
+ !source.startsWith('from', next) ||
807
+ isIdentifierChar(source[next + 'from'.length])
808
+ ) {
809
+ return undefined;
810
+ }
811
+ index = next;
812
+ continue;
813
+ }
814
+ if (
815
+ source.startsWith('from', index) &&
816
+ !isIdentifierChar(source[index - 1]) &&
817
+ !isIdentifierChar(source[index + 'from'.length])
818
+ ) {
819
+ if (!importClauseHasRuntimeBinding(source.slice(clauseStart, index))) {
820
+ return undefined;
821
+ }
822
+ index = skipTrivia(source, index + 'from'.length);
823
+ return readQuotedString(source, index)?.value;
824
+ }
825
+ if (char === ';') {
826
+ return undefined;
827
+ }
828
+ index += 1;
829
+ }
830
+
831
+ return undefined;
832
+ };
833
+
834
+ const maskSource = (source: string, options: { strings: boolean }): string => {
835
+ const output = [...source];
836
+ let index = 0;
837
+
838
+ const maskRange = (start: number, end: number): void => {
839
+ for (let cursor = start; cursor < end; cursor += 1) {
840
+ if (output[cursor] !== '\n') {
841
+ output[cursor] = ' ';
842
+ }
843
+ }
844
+ };
845
+
846
+ const skipQuoted = (quote: '"' | "'" | '`'): void => {
847
+ const start = index;
848
+ index += 1;
849
+ while (index < source.length) {
850
+ if (source[index] === '\\') {
851
+ index += 2;
852
+ continue;
853
+ }
854
+ if (source[index] === quote) {
855
+ index += 1;
856
+ break;
857
+ }
858
+ index += 1;
859
+ }
860
+ if (options.strings) {
861
+ maskRange(start, index);
862
+ }
863
+ };
864
+
865
+ while (index < source.length) {
866
+ if (source.startsWith('//', index)) {
867
+ const end = source.indexOf('\n', index + 2);
868
+ const stop = end === -1 ? source.length : end;
869
+ maskRange(index, stop);
870
+ index = stop;
871
+ continue;
872
+ }
873
+ if (source.startsWith('/*', index)) {
874
+ const end = source.indexOf('*/', index + 2);
875
+ const stop = end === -1 ? source.length : end + 2;
876
+ maskRange(index, stop);
877
+ index = stop;
878
+ continue;
879
+ }
880
+ const char = source[index];
881
+ if (char === '"' || char === "'" || char === '`') {
882
+ skipQuoted(char);
883
+ continue;
884
+ }
885
+ index += 1;
886
+ }
887
+
888
+ return output.join('');
889
+ };
890
+
891
+ const matchStartsWithAnyKeyword = (
892
+ maskedSource: string,
893
+ match: RegExpMatchArray,
894
+ keywords: readonly string[]
895
+ ): boolean => {
896
+ const index = match.index ?? 0;
897
+ return keywords.some(
898
+ (keyword) =>
899
+ maskedSource.startsWith(keyword, index) &&
900
+ !isIdentifierChar(maskedSource[index - 1]) &&
901
+ !isIdentifierChar(maskedSource[index + keyword.length])
902
+ );
903
+ };
904
+
905
+ const importsSpecifier = (
906
+ source: string,
907
+ specifier: string,
908
+ options: { includeReExports?: boolean; requireImportBinding?: boolean } = {}
909
+ ): boolean => {
910
+ const includeReExports = options.includeReExports ?? true;
911
+ const importSpecifier = options.requireImportBinding
912
+ ? boundImportDeclarationSpecifier
913
+ : importDeclarationSpecifier;
914
+ let index = 0;
915
+ while (index < source.length) {
916
+ const skippedIndex = skipImportScanIgnoredToken(source, index);
917
+ if (skippedIndex !== undefined) {
918
+ index = skippedIndex;
919
+ continue;
920
+ }
921
+
922
+ if (
923
+ source.startsWith('import', index) &&
924
+ !isIdentifierChar(source[index - 1]) &&
925
+ previousNonWhitespaceChar(source, index) !== '.' &&
926
+ !isIdentifierChar(source[index + 'import'.length])
927
+ ) {
928
+ if (importSpecifier(source, index) === specifier) {
929
+ return true;
930
+ }
931
+ index += 'import'.length;
932
+ continue;
933
+ }
934
+ if (
935
+ includeReExports &&
936
+ source.startsWith('export', index) &&
937
+ !isIdentifierChar(source[index - 1]) &&
938
+ !isIdentifierChar(source[index + 'export'.length])
939
+ ) {
940
+ if (reExportDeclarationSpecifier(source, index) === specifier) {
941
+ return true;
942
+ }
943
+ index += 'export'.length;
944
+ continue;
945
+ }
946
+ index += 1;
947
+ }
948
+
949
+ return false;
950
+ };
951
+
952
+ const pathsImporting = (
953
+ sourceFiles: readonly string[],
954
+ specifier: string,
955
+ options?: { includeReExports?: boolean; requireImportBinding?: boolean }
956
+ ): readonly string[] =>
957
+ sourceFiles.filter((filePath) =>
958
+ importsSpecifier(readFileSync(filePath, 'utf8'), specifier, options)
959
+ );
960
+
961
+ const staticImportClauseForSpecifier = (
962
+ source: string,
963
+ start: number,
964
+ specifier: string
965
+ ): string | undefined => {
966
+ let index = skipTrivia(source, start + 'import'.length);
967
+ if (
968
+ source.startsWith('type', index) &&
969
+ !isIdentifierChar(source[index + 'type'.length])
970
+ ) {
971
+ return undefined;
972
+ }
973
+ if (readQuotedString(source, index) || source[index] === '(') {
974
+ return undefined;
975
+ }
976
+
977
+ const clauseStart = index;
978
+ while (index < source.length) {
979
+ if (source.startsWith('//', index)) {
980
+ index = skipLineComment(source, index);
981
+ continue;
982
+ }
983
+ if (source.startsWith('/*', index)) {
984
+ index = skipBlockComment(source, index);
985
+ continue;
986
+ }
987
+ const char = source[index];
988
+ if (char === '"' || char === "'" || char === '`') {
989
+ index = skipQuotedLiteral(source, index);
990
+ continue;
991
+ }
992
+ if (
993
+ source.startsWith('from', index) &&
994
+ !isIdentifierChar(source[index - 1]) &&
995
+ !isIdentifierChar(source[index + 'from'.length])
996
+ ) {
997
+ const clause = source.slice(clauseStart, index);
998
+ if (!importClauseHasRuntimeBinding(clause)) {
999
+ return undefined;
1000
+ }
1001
+ index = skipTrivia(source, index + 'from'.length);
1002
+ return readQuotedString(source, index)?.value === specifier
1003
+ ? clause
1004
+ : undefined;
1005
+ }
1006
+ if (char === ';') {
1007
+ return undefined;
1008
+ }
1009
+ index += 1;
1010
+ }
1011
+
1012
+ return undefined;
1013
+ };
1014
+
1015
+ const staticImportClausesForSpecifier = (
1016
+ source: string,
1017
+ specifier: string
1018
+ ): readonly string[] => {
1019
+ const clauses: string[] = [];
1020
+ let index = 0;
1021
+ while (index < source.length) {
1022
+ if (source.startsWith('//', index)) {
1023
+ index = skipLineComment(source, index);
1024
+ continue;
1025
+ }
1026
+ if (source.startsWith('/*', index)) {
1027
+ index = skipBlockComment(source, index);
1028
+ continue;
1029
+ }
1030
+ const char = source[index];
1031
+ if (char === '"' || char === "'" || char === '`') {
1032
+ index = skipQuotedLiteral(source, index);
1033
+ continue;
1034
+ }
1035
+ if (
1036
+ source.startsWith('import', index) &&
1037
+ !isIdentifierChar(source[index - 1]) &&
1038
+ !isIdentifierChar(source[index + 'import'.length])
1039
+ ) {
1040
+ const clause = staticImportClauseForSpecifier(source, index, specifier);
1041
+ if (clause) {
1042
+ clauses.push(clause);
1043
+ }
1044
+ index += 'import'.length;
1045
+ continue;
1046
+ }
1047
+ index += 1;
1048
+ }
1049
+
1050
+ return clauses;
1051
+ };
1052
+
1053
+ const namedImportBindings = (
1054
+ source: string,
1055
+ specifier: string,
1056
+ exportedName: string
1057
+ ): readonly string[] => {
1058
+ const namedBindings: string[] = [];
1059
+ const namespaceBindings: string[] = [];
1060
+ for (const clause of staticImportClausesForSpecifier(source, specifier)) {
1061
+ const code = maskSource(clause, { strings: false });
1062
+ const namespaceImport =
1063
+ /(?:^|,)\s*\*\s+as\s+(?<local>[A-Za-z_$][\w$]*)(?:\s*$|,)/u.exec(
1064
+ code
1065
+ )?.groups;
1066
+ if (namespaceImport?.['local']) {
1067
+ namespaceBindings.push(`${namespaceImport['local']}.${exportedName}`);
1068
+ }
1069
+
1070
+ const namedImports = /\{(?<imports>[\s\S]*?)\}/u.exec(code)?.groups?.[
1071
+ 'imports'
1072
+ ];
1073
+ if (!namedImports) {
1074
+ continue;
1075
+ }
1076
+
1077
+ for (const item of namedImports.split(',')) {
1078
+ const specifierText = item.trim();
1079
+ if (specifierText.length === 0 || specifierText.startsWith('type ')) {
1080
+ continue;
1081
+ }
1082
+
1083
+ const imported =
1084
+ /^(?<imported>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<local>[A-Za-z_$][\w$]*))?$/u.exec(
1085
+ specifierText
1086
+ )?.groups;
1087
+ if (imported?.['imported'] === exportedName) {
1088
+ namedBindings.push(imported['local'] ?? imported['imported']);
1089
+ }
1090
+ }
1091
+ }
1092
+
1093
+ return [...new Set([...namedBindings, ...namespaceBindings])];
1094
+ };
1095
+
1096
+ const dynamicImportNamespaceBindings = (
1097
+ source: string,
1098
+ specifier: string
1099
+ ): readonly string[] => {
1100
+ const code = maskSource(source, { strings: false });
1101
+ const stringsMaskedCode = maskSource(source, { strings: true });
1102
+ const escapedSpecifier = escapeRegExp(specifier);
1103
+ const pattern = new RegExp(
1104
+ `\\b(?:const|let|var)\\s+(?<local>[A-Za-z_$][\\w$]*)\\s*=\\s*(?:await\\s+)?import\\s*\\(\\s*['"]${escapedSpecifier}['"]\\s*\\)`,
1105
+ 'gu'
1106
+ );
1107
+
1108
+ return [...code.matchAll(pattern)]
1109
+ .filter((match) =>
1110
+ matchStartsWithAnyKeyword(stringsMaskedCode, match, [
1111
+ 'const',
1112
+ 'let',
1113
+ 'var',
1114
+ ])
1115
+ )
1116
+ .map((match) => match.groups?.['local'])
1117
+ .filter((local): local is string => local !== undefined);
1118
+ };
1119
+
1120
+ const dynamicImportNamedBinding = (
1121
+ source: string,
1122
+ specifier: string,
1123
+ exportedName: string
1124
+ ): string | undefined => {
1125
+ const code = maskSource(source, { strings: false });
1126
+ const stringsMaskedCode = maskSource(source, { strings: true });
1127
+ const escapedSpecifier = escapeRegExp(specifier);
1128
+ const pattern = new RegExp(
1129
+ `\\b(?:const|let|var)\\s*\\{(?<imports>[\\s\\S]*?)\\}\\s*=\\s*(?:await\\s+)?import\\s*\\(\\s*['"]${escapedSpecifier}['"]\\s*\\)`,
1130
+ 'gu'
1131
+ );
1132
+
1133
+ for (const match of code.matchAll(pattern)) {
1134
+ if (
1135
+ !matchStartsWithAnyKeyword(stringsMaskedCode, match, [
1136
+ 'const',
1137
+ 'let',
1138
+ 'var',
1139
+ ])
1140
+ ) {
1141
+ continue;
1142
+ }
1143
+
1144
+ const namedImports = match.groups?.['imports'] ?? '';
1145
+ for (const item of namedImports.split(',')) {
1146
+ const specifierText = item.trim();
1147
+ if (specifierText.length === 0 || specifierText.startsWith('...')) {
1148
+ continue;
1149
+ }
1150
+
1151
+ const imported =
1152
+ /^(?<imported>[A-Za-z_$][\w$]*)(?:\s*:\s*(?<local>[A-Za-z_$][\w$]*))?(?:\s*=.*)?$/u.exec(
1153
+ specifierText
1154
+ )?.groups;
1155
+ if (imported?.['imported'] === exportedName) {
1156
+ return imported['local'] ?? imported['imported'];
1157
+ }
1158
+ }
1159
+ }
1160
+
1161
+ return undefined;
1162
+ };
1163
+
1164
+ interface LocalValueExport {
1165
+ readonly identifier: string;
1166
+ readonly sourcePath: string;
1167
+ }
1168
+
1169
+ interface LocalReexport {
1170
+ readonly identifier: string;
1171
+ readonly specifier: string;
1172
+ readonly typeOnly: boolean;
1173
+ }
1174
+
1175
+ interface LocalImport {
1176
+ readonly identifier: string;
1177
+ readonly specifier: string;
1178
+ readonly typeOnly: boolean;
1179
+ }
1180
+
1181
+ interface NamedExportItem {
1182
+ readonly local: string;
1183
+ readonly name: string;
1184
+ readonly typeOnly: boolean;
1185
+ }
1186
+
1187
+ const parseNamedExportItem = (
1188
+ item: string,
1189
+ declarationTypeOnly: boolean
1190
+ ): NamedExportItem | undefined => {
1191
+ const trimmedItem = item.trim();
1192
+ if (!trimmedItem) {
1193
+ return undefined;
1194
+ }
1195
+
1196
+ const itemTypeOnly = declarationTypeOnly || trimmedItem.startsWith('type ');
1197
+ const specifierText = trimmedItem.replace(/^type\s+/u, '');
1198
+ const exported =
1199
+ /^(?<local>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<name>[A-Za-z_$][\w$]*))?$/u.exec(
1200
+ specifierText
1201
+ )?.groups;
1202
+ if (!exported?.['local']) {
1203
+ return undefined;
1204
+ }
1205
+
1206
+ return {
1207
+ local: exported['local'],
1208
+ name: exported['name'] ?? exported['local'],
1209
+ typeOnly: itemTypeOnly,
1210
+ };
1211
+ };
1212
+
1213
+ const declaresValueBinding = (source: string, identifier: string): boolean => {
1214
+ const code = maskSource(source, { strings: true });
1215
+ const escapedIdentifier = escapeRegExp(identifier);
1216
+ return new RegExp(
1217
+ `\\b(?:export\\s+)?(?:(?:async\\s+)?function|const|let|var|class|enum)\\s+${escapedIdentifier}\\b`,
1218
+ 'u'
1219
+ ).test(code);
1220
+ };
1221
+
1222
+ const declaresValueExport = (source: string, identifier: string): boolean => {
1223
+ const code = maskSource(source, { strings: true });
1224
+ const escapedIdentifier = escapeRegExp(identifier);
1225
+ return new RegExp(
1226
+ `\\bexport\\s+(?:(?:async\\s+)?function|const|let|var|class|enum)\\s+${escapedIdentifier}\\b`,
1227
+ 'u'
1228
+ ).test(code);
1229
+ };
1230
+
1231
+ const sameFileValueExportLocal = (
1232
+ source: string,
1233
+ identifier: string
1234
+ ): string | undefined => {
1235
+ const code = maskSource(source, { strings: true });
1236
+ const pattern =
1237
+ /\bexport\s+(?<typeOnly>type\s+)?\{(?<exports>[\s\S]*?)\}(?!\s+from\b)/gu;
1238
+
1239
+ for (const match of code.matchAll(pattern)) {
1240
+ const declarationTypeOnly = Boolean(match.groups?.['typeOnly']);
1241
+ const namedExports = match.groups?.['exports'] ?? '';
1242
+ for (const item of namedExports.split(',')) {
1243
+ const exported = parseNamedExportItem(item, declarationTypeOnly);
1244
+ if (!exported || exported.typeOnly || exported.name !== identifier) {
1245
+ continue;
1246
+ }
1247
+
1248
+ return exported.local;
1249
+ }
1250
+ }
1251
+
1252
+ return undefined;
1253
+ };
1254
+
1255
+ const namedLocalImports = (
1256
+ source: string,
1257
+ identifier: string
1258
+ ): readonly LocalImport[] => {
1259
+ const code = maskSource(source, { strings: false });
1260
+ const stringsMaskedCode = maskSource(source, { strings: true });
1261
+ const imports: LocalImport[] = [];
1262
+ const pattern =
1263
+ /\bimport\s+(?<typeOnly>type\s+)?\{(?<imports>[\s\S]*?)\}\s+from\s+['"](?<specifier>[^'"]+)['"]/gu;
1264
+
1265
+ for (const match of code.matchAll(pattern)) {
1266
+ if (!stringsMaskedCode.startsWith('import', match.index ?? 0)) {
1267
+ continue;
1268
+ }
1269
+
1270
+ const specifier = match.groups?.['specifier'];
1271
+ if (!specifier?.startsWith('.')) {
1272
+ continue;
1273
+ }
1274
+
1275
+ const namedImports = match.groups?.['imports'] ?? '';
1276
+ for (const item of namedImports.split(',')) {
1277
+ const trimmedItem = item.trim();
1278
+ if (!trimmedItem) {
1279
+ continue;
1280
+ }
1281
+
1282
+ const itemTypeOnly =
1283
+ Boolean(match.groups?.['typeOnly']) || trimmedItem.startsWith('type ');
1284
+ const specifierText = trimmedItem.replace(/^type\s+/u, '');
1285
+ const imported =
1286
+ /^(?<imported>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<local>[A-Za-z_$][\w$]*))?$/u.exec(
1287
+ specifierText
1288
+ )?.groups;
1289
+ if (
1290
+ !imported?.['imported'] ||
1291
+ (imported['local'] ?? imported['imported']) !== identifier
1292
+ ) {
1293
+ continue;
1294
+ }
1295
+
1296
+ imports.push({
1297
+ identifier: imported['imported'],
1298
+ specifier,
1299
+ typeOnly: itemTypeOnly,
1300
+ });
1301
+ }
1302
+ }
1303
+
1304
+ return imports;
1305
+ };
1306
+
1307
+ const namedLocalReexports = (
1308
+ source: string,
1309
+ identifier: string
1310
+ ): readonly LocalReexport[] => {
1311
+ const code = maskSource(source, { strings: false });
1312
+ const stringsMaskedCode = maskSource(source, { strings: true });
1313
+ const exports: LocalReexport[] = [];
1314
+ const pattern =
1315
+ /\bexport\s+(?<typeOnly>type\s+)?\{(?<exports>[\s\S]*?)\}\s+from\s+['"](?<specifier>[^'"]+)['"]/gu;
1316
+
1317
+ for (const match of code.matchAll(pattern)) {
1318
+ if (!matchStartsWithAnyKeyword(stringsMaskedCode, match, ['export'])) {
1319
+ continue;
1320
+ }
1321
+
1322
+ const specifier = match.groups?.['specifier'];
1323
+ if (!specifier?.startsWith('.')) {
1324
+ continue;
1325
+ }
1326
+
1327
+ const namedExports = match.groups?.['exports'] ?? '';
1328
+ for (const item of namedExports.split(',')) {
1329
+ const exported = parseNamedExportItem(
1330
+ item,
1331
+ Boolean(match.groups?.['typeOnly'])
1332
+ );
1333
+ if (!exported || exported.name !== identifier) {
1334
+ continue;
1335
+ }
1336
+
1337
+ exports.push({
1338
+ identifier: exported.local,
1339
+ specifier,
1340
+ typeOnly: exported.typeOnly,
1341
+ });
1342
+ }
1343
+ }
1344
+
1345
+ return exports;
1346
+ };
1347
+
1348
+ const starLocalReexports = (source: string): readonly LocalReexport[] => {
1349
+ const code = maskSource(source, { strings: false });
1350
+ const stringsMaskedCode = maskSource(source, { strings: true });
1351
+ return [
1352
+ ...code.matchAll(
1353
+ /\bexport\s+(?<typeOnly>type\s+)?\*\s+from\s+['"](?<specifier>[^'"]+)['"]/gu
1354
+ ),
1355
+ ]
1356
+ .filter((match) =>
1357
+ matchStartsWithAnyKeyword(stringsMaskedCode, match, ['export'])
1358
+ )
1359
+ .map((match) => ({
1360
+ identifier: '',
1361
+ specifier: match.groups?.['specifier'] ?? '',
1362
+ typeOnly: Boolean(match.groups?.['typeOnly']),
1363
+ }))
1364
+ .filter((entry) => entry.specifier.startsWith('.'));
1365
+ };
1366
+
1367
+ const resolveLocalModuleSpecifier = (
1368
+ sourcePath: string,
1369
+ specifier: string
1370
+ ): string | undefined => {
1371
+ const basePath = resolve(dirname(sourcePath), specifier);
1372
+ const candidates = [
1373
+ basePath,
1374
+ basePath.endsWith('.js') ? `${basePath.slice(0, -3)}.ts` : undefined,
1375
+ basePath.endsWith('.js') ? `${basePath.slice(0, -3)}.tsx` : undefined,
1376
+ basePath.endsWith('.mjs') ? `${basePath.slice(0, -4)}.mts` : undefined,
1377
+ `${basePath}.ts`,
1378
+ `${basePath}.tsx`,
1379
+ join(basePath, 'index.ts'),
1380
+ join(basePath, 'index.tsx'),
1381
+ ].filter((candidate): candidate is string => candidate !== undefined);
1382
+
1383
+ return candidates.find((candidate) => existsSync(candidate));
1384
+ };
1385
+
1386
+ const resolveLocalValueExport = (
1387
+ sourcePath: string,
1388
+ identifier: string,
1389
+ visited = new Set<string>()
1390
+ ): LocalValueExport | undefined => {
1391
+ const normalizedSourcePath = normalizeRealPath(sourcePath);
1392
+ const visitKey = `${normalizedSourcePath}:${identifier}`;
1393
+ if (visited.has(visitKey)) {
1394
+ return undefined;
1395
+ }
1396
+ visited.add(visitKey);
1397
+
1398
+ let source: string;
1399
+ try {
1400
+ source = readFileSync(normalizedSourcePath, 'utf8');
1401
+ } catch {
1402
+ return undefined;
1403
+ }
1404
+
1405
+ if (declaresValueExport(source, identifier)) {
1406
+ return { identifier, sourcePath: normalizedSourcePath };
1407
+ }
1408
+
1409
+ const sameFileLocal = sameFileValueExportLocal(source, identifier);
1410
+ if (sameFileLocal && declaresValueBinding(source, sameFileLocal)) {
1411
+ return { identifier: sameFileLocal, sourcePath: normalizedSourcePath };
1412
+ }
1413
+ if (sameFileLocal) {
1414
+ for (const localImport of namedLocalImports(source, sameFileLocal)) {
1415
+ if (localImport.typeOnly) {
1416
+ continue;
1417
+ }
1418
+ const targetPath = resolveLocalModuleSpecifier(
1419
+ normalizedSourcePath,
1420
+ localImport.specifier
1421
+ );
1422
+ const resolved =
1423
+ targetPath &&
1424
+ resolveLocalValueExport(targetPath, localImport.identifier, visited);
1425
+ if (resolved) {
1426
+ return resolved;
1427
+ }
1428
+ }
1429
+ }
1430
+
1431
+ for (const reexport of namedLocalReexports(source, identifier)) {
1432
+ if (reexport.typeOnly) {
1433
+ continue;
1434
+ }
1435
+ const targetPath = resolveLocalModuleSpecifier(
1436
+ normalizedSourcePath,
1437
+ reexport.specifier
1438
+ );
1439
+ const resolved =
1440
+ targetPath &&
1441
+ resolveLocalValueExport(targetPath, reexport.identifier, visited);
1442
+ if (resolved) {
1443
+ return resolved;
1444
+ }
1445
+ }
1446
+
1447
+ for (const reexport of starLocalReexports(source)) {
1448
+ if (reexport.typeOnly) {
1449
+ continue;
1450
+ }
1451
+ const targetPath = resolveLocalModuleSpecifier(
1452
+ normalizedSourcePath,
1453
+ reexport.specifier
1454
+ );
1455
+ const resolved =
1456
+ targetPath && resolveLocalValueExport(targetPath, identifier, visited);
1457
+ if (resolved) {
1458
+ return resolved;
1459
+ }
1460
+ }
1461
+
1462
+ return undefined;
1463
+ };
1464
+
1465
+ const findClosingParen = (source: string, openIndex: number): number => {
1466
+ let depth = 0;
1467
+ for (let index = openIndex; index < source.length; index += 1) {
1468
+ if (source[index] === '(') {
1469
+ depth += 1;
1470
+ continue;
1471
+ }
1472
+ if (source[index] !== ')') {
1473
+ continue;
1474
+ }
1475
+ depth -= 1;
1476
+ if (depth === 0) {
1477
+ return index;
1478
+ }
1479
+ }
1480
+
1481
+ return -1;
1482
+ };
1483
+
1484
+ const previousNonWhitespace = (source: string, index: number): string => {
1485
+ let previousIndex = index - 1;
1486
+ while (previousIndex >= 0 && /\s/u.test(source[previousIndex] ?? '')) {
1487
+ previousIndex -= 1;
1488
+ }
1489
+ return source[previousIndex] ?? '';
1490
+ };
1491
+
1492
+ const containsCall = (source: string, identifier: string): boolean => {
1493
+ const escapedIdentifier = escapeRegExp(identifier);
1494
+ const callPattern = new RegExp(`\\b${escapedIdentifier}\\s*\\(`, 'gu');
1495
+ for (const match of source.matchAll(callPattern)) {
1496
+ if (previousNonWhitespace(source, match.index ?? 0) !== '.') {
1497
+ return true;
1498
+ }
1499
+ }
1500
+ return false;
1501
+ };
1502
+
1503
+ const splitTopLevelArguments = (source: string): readonly string[] => {
1504
+ const args: string[] = [];
1505
+ let start = 0;
1506
+ let parenDepth = 0;
1507
+ let braceDepth = 0;
1508
+ let bracketDepth = 0;
1509
+
1510
+ for (let index = 0; index < source.length; index += 1) {
1511
+ const char = source[index];
1512
+ if (char === '(') {
1513
+ parenDepth += 1;
1514
+ continue;
1515
+ }
1516
+ if (char === ')') {
1517
+ parenDepth = Math.max(0, parenDepth - 1);
1518
+ continue;
1519
+ }
1520
+ if (char === '{') {
1521
+ braceDepth += 1;
1522
+ continue;
1523
+ }
1524
+ if (char === '}') {
1525
+ braceDepth = Math.max(0, braceDepth - 1);
1526
+ continue;
1527
+ }
1528
+ if (char === '[') {
1529
+ bracketDepth += 1;
1530
+ continue;
1531
+ }
1532
+ if (char === ']') {
1533
+ bracketDepth = Math.max(0, bracketDepth - 1);
1534
+ continue;
1535
+ }
1536
+ if (
1537
+ char === ',' &&
1538
+ parenDepth === 0 &&
1539
+ braceDepth === 0 &&
1540
+ bracketDepth === 0
1541
+ ) {
1542
+ args.push(source.slice(start, index).trim());
1543
+ start = index + 1;
1544
+ }
1545
+ }
1546
+
1547
+ const trailing = source.slice(start).trim();
1548
+ if (trailing || args.length > 0) {
1549
+ args.push(trailing);
1550
+ }
1551
+ return args;
1552
+ };
1553
+
1554
+ const runnerCallArguments = (
1555
+ source: string,
1556
+ runner: string
1557
+ ): readonly (readonly string[])[] => {
1558
+ const code = maskSource(source, { strings: true });
1559
+ const escapedRunner = escapeRegExp(runner);
1560
+ const runnerPattern = new RegExp(`\\b${escapedRunner}\\s*\\(`, 'gu');
1561
+ const calls: (readonly string[])[] = [];
1562
+
1563
+ for (const match of code.matchAll(runnerPattern)) {
1564
+ const matchIndex = match.index ?? 0;
1565
+ if (previousNonWhitespace(code, matchIndex) === '.') {
1566
+ continue;
1567
+ }
1568
+
1569
+ const openIndex = code.indexOf('(', matchIndex);
1570
+ const closeIndex = findClosingParen(code, openIndex);
1571
+ if (closeIndex === -1) {
1572
+ continue;
1573
+ }
1574
+ calls.push(splitTopLevelArguments(code.slice(openIndex + 1, closeIndex)));
1575
+ }
1576
+
1577
+ return calls;
1578
+ };
1579
+
1580
+ const isSentinelAdapterArgument = (argument: string): boolean =>
1581
+ /^(?:undefined|null|void\s*(?:0|\(0\)))$/u.test(argument.trim());
1582
+
1583
+ const runnerInvokesCasesFactory = (
1584
+ source: string,
1585
+ runner: string,
1586
+ casesFactory: string
1587
+ ): boolean =>
1588
+ runnerCallArguments(source, runner).some((args) => {
1589
+ const adapterArgument = args[0]?.trim();
1590
+ return (
1591
+ adapterArgument !== undefined &&
1592
+ adapterArgument.length > 0 &&
1593
+ !isSentinelAdapterArgument(adapterArgument) &&
1594
+ !containsCall(adapterArgument, casesFactory) &&
1595
+ args.slice(1).some((argument) => containsCall(argument, casesFactory))
1596
+ );
1597
+ });
1598
+
1599
+ const runnerInvokedWithAdapterArgument = (
1600
+ source: string,
1601
+ runner: string,
1602
+ casesFactories: readonly string[] = []
1603
+ ): boolean =>
1604
+ runnerCallArguments(source, runner).some((args) => {
1605
+ const adapterArgument = args[0]?.trim();
1606
+ return (
1607
+ adapterArgument !== undefined &&
1608
+ adapterArgument.length > 0 &&
1609
+ !isSentinelAdapterArgument(adapterArgument) &&
1610
+ casesFactories.every(
1611
+ (casesFactory) => !containsCall(adapterArgument, casesFactory)
1612
+ )
1613
+ );
1614
+ });
1615
+
1616
+ const ownerRunnerDefaultsCasesFactory = (
1617
+ targetEntry: AdapterTargetCatalogEntry
1618
+ ): boolean => {
1619
+ const { conformance, testingExportTarget } = targetEntry;
1620
+ if (!conformance || !testingExportTarget) {
1621
+ return false;
1622
+ }
1623
+
1624
+ const runnerExport = resolveLocalValueExport(
1625
+ testingExportTarget,
1626
+ conformance.runner
1627
+ );
1628
+ if (!runnerExport) {
1629
+ return false;
1630
+ }
1631
+ const casesFactoryExport = resolveLocalValueExport(
1632
+ testingExportTarget,
1633
+ conformance.casesFactory
1634
+ );
1635
+ const casesFactoryIdentifiers = [
1636
+ conformance.casesFactory,
1637
+ casesFactoryExport?.identifier,
1638
+ ].filter((identifier): identifier is string => identifier !== undefined);
1639
+
1640
+ let source: string;
1641
+ try {
1642
+ source = readFileSync(runnerExport.sourcePath, 'utf8');
1643
+ } catch {
1644
+ return false;
1645
+ }
1646
+
1647
+ const code = maskSource(source, { strings: true });
1648
+ const escapedRunner = escapeRegExp(runnerExport.identifier);
1649
+ const declarationPatterns = [
1650
+ new RegExp(
1651
+ `\\b(?:export\\s+)?(?:async\\s+)?function\\s+${escapedRunner}\\b`,
1652
+ 'gu'
1653
+ ),
1654
+ new RegExp(
1655
+ `\\b(?:export\\s+)?(?:const|let|var)\\s+${escapedRunner}\\b\\s*(?::[^=;]*)?=`,
1656
+ 'gu'
1657
+ ),
1658
+ ];
1659
+
1660
+ for (const pattern of declarationPatterns) {
1661
+ for (const match of code.matchAll(pattern)) {
1662
+ const openIndex = code.indexOf('(', (match.index ?? 0) + match[0].length);
1663
+ if (openIndex === -1) {
1664
+ continue;
1665
+ }
1666
+ const closeIndex = findClosingParen(code, openIndex);
1667
+ if (closeIndex === -1) {
1668
+ continue;
1669
+ }
1670
+
1671
+ const params = code.slice(openIndex + 1, closeIndex);
1672
+ if (
1673
+ params.includes('=') &&
1674
+ casesFactoryIdentifiers.some((identifier) =>
1675
+ containsCall(params, identifier)
1676
+ )
1677
+ ) {
1678
+ return true;
1679
+ }
1680
+ }
1681
+ }
1682
+
1683
+ return false;
1684
+ };
1685
+
1686
+ const runnerBindingProvesConformance = (
1687
+ source: string,
1688
+ targetEntry: AdapterTargetCatalogEntry,
1689
+ runnerBinding: string,
1690
+ casesFactoryBindings: readonly string[]
1691
+ ): boolean => {
1692
+ for (const casesFactoryBinding of casesFactoryBindings) {
1693
+ if (runnerInvokesCasesFactory(source, runnerBinding, casesFactoryBinding)) {
1694
+ return true;
1695
+ }
1696
+ }
1697
+
1698
+ return (
1699
+ ownerRunnerDefaultsCasesFactory(targetEntry) &&
1700
+ runnerInvokedWithAdapterArgument(
1701
+ source,
1702
+ runnerBinding,
1703
+ casesFactoryBindings
1704
+ )
1705
+ );
1706
+ };
1707
+
1708
+ const provesConformance = (
1709
+ source: string,
1710
+ targetEntry: AdapterTargetCatalogEntry
1711
+ ): boolean => {
1712
+ const { conformance, testingImport } = targetEntry;
1713
+ if (
1714
+ !testingImport ||
1715
+ !importsSpecifier(source, testingImport, {
1716
+ includeReExports: false,
1717
+ requireImportBinding: true,
1718
+ })
1719
+ ) {
1720
+ return false;
1721
+ }
1722
+
1723
+ if (!conformance) {
1724
+ return true;
1725
+ }
1726
+
1727
+ const runnerBindings = namedImportBindings(
1728
+ source,
1729
+ testingImport,
1730
+ conformance.runner
1731
+ );
1732
+ const casesFactoryBindings = namedImportBindings(
1733
+ source,
1734
+ testingImport,
1735
+ conformance.casesFactory
1736
+ );
1737
+ if (runnerBindings.length === 0) {
1738
+ const dynamicRunnerBinding = dynamicImportNamedBinding(
1739
+ source,
1740
+ testingImport,
1741
+ conformance.runner
1742
+ );
1743
+ const dynamicCasesFactoryBinding = dynamicImportNamedBinding(
1744
+ source,
1745
+ testingImport,
1746
+ conformance.casesFactory
1747
+ );
1748
+ const dynamicCasesFactoryBindings = dynamicCasesFactoryBinding
1749
+ ? [dynamicCasesFactoryBinding]
1750
+ : [];
1751
+ if (
1752
+ dynamicRunnerBinding &&
1753
+ runnerBindingProvesConformance(
1754
+ source,
1755
+ targetEntry,
1756
+ dynamicRunnerBinding,
1757
+ dynamicCasesFactoryBindings
1758
+ )
1759
+ ) {
1760
+ return true;
1761
+ }
1762
+
1763
+ for (const namespaceBinding of dynamicImportNamespaceBindings(
1764
+ source,
1765
+ testingImport
1766
+ )) {
1767
+ const namespaceRunnerBinding = `${namespaceBinding}.${conformance.runner}`;
1768
+ const namespaceCasesFactoryBinding = `${namespaceBinding}.${conformance.casesFactory}`;
1769
+ if (
1770
+ runnerBindingProvesConformance(
1771
+ source,
1772
+ targetEntry,
1773
+ namespaceRunnerBinding,
1774
+ [namespaceCasesFactoryBinding]
1775
+ )
1776
+ ) {
1777
+ return true;
1778
+ }
1779
+ }
1780
+
1781
+ return false;
1782
+ }
1783
+ return runnerBindings.some((runnerBinding) =>
1784
+ runnerBindingProvesConformance(
1785
+ source,
1786
+ targetEntry,
1787
+ runnerBinding,
1788
+ casesFactoryBindings
1789
+ )
1790
+ );
1791
+ };
1792
+
1793
+ const pathsProvingConformance = (
1794
+ sourceFiles: readonly string[],
1795
+ targetEntry: AdapterTargetCatalogEntry
1796
+ ): readonly string[] =>
1797
+ sourceFiles.filter((filePath) =>
1798
+ provesConformance(readFileSync(filePath, 'utf8'), targetEntry)
1799
+ );
1800
+
1801
+ const isTestFile = (filePath: string): boolean => {
1802
+ const normalizedPath = normalizePath(filePath);
1803
+ return (
1804
+ normalizedPath.includes('/__tests__/') ||
1805
+ normalizedPath.endsWith('.test.ts') ||
1806
+ normalizedPath.endsWith('.test-d.ts')
1807
+ );
1808
+ };
1809
+
1810
+ const assertPackageExports = (
1811
+ workspace: WorkspacePackage,
1812
+ diagnostics: AdapterCheckDiagnostic[]
1813
+ ): void => {
1814
+ const packageName = workspace.manifest.name as string;
1815
+ for (const key of ['.', './package.json'] as const) {
1816
+ if (hasResolvableExport(workspace, key)) {
1817
+ continue;
1818
+ }
1819
+ diagnostics.push(
1820
+ diagnostic(
1821
+ workspace.packageJsonPath,
1822
+ packageName,
1823
+ 'missing-package-export',
1824
+ `${packageName} must export "${key}" so adapter kit and consumers can resolve it.`
1825
+ )
1826
+ );
1827
+ }
1828
+ };
1829
+
1830
+ const assertDependencyDirection = (
1831
+ workspace: WorkspacePackage,
1832
+ targetEntry: AdapterTargetCatalogEntry,
1833
+ diagnostics: AdapterCheckDiagnostic[]
1834
+ ): void => {
1835
+ const packageName = workspace.manifest.name as string;
1836
+ const dependencies = dependencyMap(workspace.manifest.dependencies);
1837
+ const devDependencies = dependencyMap(workspace.manifest.devDependencies);
1838
+ const optionalDependencies = dependencyMap(
1839
+ workspace.manifest.optionalDependencies
1840
+ );
1841
+ const peerDependencies = dependencyMap(workspace.manifest.peerDependencies);
1842
+
1843
+ if (
1844
+ Object.hasOwn(dependencies, targetEntry.ownerPackage) ||
1845
+ Object.hasOwn(optionalDependencies, targetEntry.ownerPackage)
1846
+ ) {
1847
+ diagnostics.push(
1848
+ diagnostic(
1849
+ workspace.packageJsonPath,
1850
+ packageName,
1851
+ 'dependency-direction',
1852
+ `${packageName} must peer-depend on ${targetEntry.ownerPackage}; runtime dependencies invert the adapter boundary.`,
1853
+ targetEntry.target,
1854
+ 'extracted'
1855
+ )
1856
+ );
1857
+ }
1858
+
1859
+ if (Object.hasOwn(devDependencies, targetEntry.ownerPackage)) {
1860
+ diagnostics.push(
1861
+ diagnostic(
1862
+ workspace.packageJsonPath,
1863
+ packageName,
1864
+ 'dependency-direction',
1865
+ `${packageName} must not hide ${targetEntry.ownerPackage} in devDependencies; declare the owner as a peer dependency.`,
1866
+ targetEntry.target,
1867
+ 'extracted'
1868
+ )
1869
+ );
1870
+ }
1871
+
1872
+ if (!Object.hasOwn(peerDependencies, targetEntry.ownerPackage)) {
1873
+ diagnostics.push(
1874
+ diagnostic(
1875
+ workspace.packageJsonPath,
1876
+ packageName,
1877
+ 'dependency-direction',
1878
+ `${packageName} must declare ${targetEntry.ownerPackage} in peerDependencies for extracted adapter placement.`,
1879
+ targetEntry.target,
1880
+ 'extracted'
1881
+ )
1882
+ );
1883
+ }
1884
+ };
1885
+
1886
+ const assertToolingBoundary = (
1887
+ workspace: WorkspacePackage,
1888
+ sourceFiles: readonly string[],
1889
+ diagnostics: AdapterCheckDiagnostic[]
1890
+ ): void => {
1891
+ const packageName = workspace.manifest.name as string;
1892
+ if (runtimeDependencyNames(workspace.manifest).has(adapterKitPackageName)) {
1893
+ diagnostics.push(
1894
+ diagnostic(
1895
+ workspace.packageJsonPath,
1896
+ packageName,
1897
+ 'tooling-boundary',
1898
+ `${packageName} must not depend on ${adapterKitPackageName}; adapter kit stays out of runtime adapter packages.`
1899
+ )
1900
+ );
1901
+ }
1902
+
1903
+ const runtimeSourceFiles = sourceFiles.filter(
1904
+ (sourceFile) => !isTestFile(sourceFile)
1905
+ );
1906
+ const toolingImportPaths = pathsImporting(
1907
+ runtimeSourceFiles,
1908
+ adapterKitPackageName
1909
+ );
1910
+ for (const sourcePath of toolingImportPaths) {
1911
+ diagnostics.push(
1912
+ diagnostic(
1913
+ workspace.packageJsonPath,
1914
+ packageName,
1915
+ 'tooling-boundary',
1916
+ `${packageName} imports ${adapterKitPackageName} from ${normalizePath(relative(workspace.packageRoot, sourcePath))}; adapters must not import the adapter kit engine.`
1917
+ )
1918
+ );
1919
+ }
1920
+ };
1921
+
1922
+ const checkAdapterPackage = (
1923
+ workspace: WorkspacePackage,
1924
+ targetById: ReadonlyMap<string, AdapterTargetCatalogEntry>
1925
+ ): {
1926
+ readonly diagnostics: readonly AdapterCheckDiagnostic[];
1927
+ readonly subject?: AdapterCheckSubject | undefined;
1928
+ } => {
1929
+ const packageName = workspace.manifest.name as string;
1930
+ const diagnostics: AdapterCheckDiagnostic[] = [];
1931
+ const placement = placementForWorkspace(workspace.workspacePath);
1932
+ const metadata = trailAdapterMetadata(workspace.manifest);
1933
+
1934
+ if (!placement || metadata === undefined) {
1935
+ return { diagnostics: [] };
1936
+ }
1937
+
1938
+ assertPackageExports(workspace, diagnostics);
1939
+ const sourceFiles = collectSourceFiles(join(workspace.packageRoot, 'src'));
1940
+ assertToolingBoundary(workspace, sourceFiles, diagnostics);
1941
+
1942
+ if (metadata === null) {
1943
+ return {
1944
+ diagnostics: [
1945
+ ...diagnostics,
1946
+ diagnostic(
1947
+ workspace.packageJsonPath,
1948
+ packageName,
1949
+ 'invalid-adapter-metadata',
1950
+ `${packageName} must declare trails.adapter as an object with a kebab-case target string.`,
1951
+ undefined,
1952
+ placement
1953
+ ),
1954
+ ],
1955
+ };
1956
+ }
1957
+
1958
+ const targetEntry = targetById.get(metadata.target);
1959
+ if (!targetEntry) {
1960
+ return {
1961
+ diagnostics: [
1962
+ ...diagnostics,
1963
+ diagnostic(
1964
+ workspace.packageJsonPath,
1965
+ packageName,
1966
+ 'unknown-adapter-target',
1967
+ `${packageName} declares unknown adapter target "${metadata.target}".`,
1968
+ metadata.target,
1969
+ placement
1970
+ ),
1971
+ ],
1972
+ };
1973
+ }
1974
+
1975
+ if (!targetEntry.placements.includes(placement)) {
1976
+ diagnostics.push(
1977
+ diagnostic(
1978
+ workspace.packageJsonPath,
1979
+ packageName,
1980
+ 'unsupported-placement',
1981
+ `${targetEntry.ownerPackage}:${targetEntry.target} does not support ${placement} adapter placement.`,
1982
+ targetEntry.target,
1983
+ placement
1984
+ )
1985
+ );
1986
+ }
1987
+
1988
+ if (placement === 'extracted') {
1989
+ assertDependencyDirection(workspace, targetEntry, diagnostics);
1990
+ }
1991
+
1992
+ const { conformance, testingImport } = targetEntry;
1993
+ const conformanceTestPaths = testingImport
1994
+ ? pathsProvingConformance(sourceFiles.filter(isTestFile), targetEntry)
1995
+ : [];
1996
+
1997
+ if (!testingImport) {
1998
+ diagnostics.push(
1999
+ diagnostic(
2000
+ workspace.packageJsonPath,
2001
+ packageName,
2002
+ 'missing-owner-conformance',
2003
+ `${targetEntry.ownerPackage}:${targetEntry.target} must declare testingImport before adapters can prove conformance.`,
2004
+ targetEntry.target,
2005
+ placement
2006
+ )
2007
+ );
2008
+ } else if (conformanceTestPaths.length === 0) {
2009
+ const conformanceHint = conformance
2010
+ ? ` and call ${conformance.runner}(adapter, ${conformance.casesFactory}(...))`
2011
+ : '';
2012
+ diagnostics.push(
2013
+ diagnostic(
2014
+ workspace.packageJsonPath,
2015
+ packageName,
2016
+ 'missing-conformance',
2017
+ `${packageName} must import ${testingImport} from a conformance test${conformanceHint}.`,
2018
+ targetEntry.target,
2019
+ placement
2020
+ )
2021
+ );
2022
+ }
2023
+
2024
+ return {
2025
+ diagnostics,
2026
+ subject: {
2027
+ conformanceTestPaths,
2028
+ key: packageName,
2029
+ ownerPackage: targetEntry.ownerPackage,
2030
+ packageJsonPath: workspace.packageJsonPath,
2031
+ packageName,
2032
+ packageRoot: workspace.packageRoot,
2033
+ placement,
2034
+ target: targetEntry.target,
2035
+ targetKey: targetEntry.key,
2036
+ ...(testingImport ? { testingImport } : {}),
2037
+ },
2038
+ };
2039
+ };
2040
+
2041
+ export const checkAdapters = (rootDir: string): AdapterCheckReport => {
2042
+ const catalog = deriveAdapterTargetCatalog(rootDir);
2043
+ const targetById = targetEntriesByTarget(catalog.targets);
2044
+ const diagnostics: AdapterCheckDiagnostic[] = [
2045
+ ...catalogDiagnostics(catalog),
2046
+ ];
2047
+ const subjects: AdapterCheckSubject[] = [];
2048
+
2049
+ for (const workspace of workspacePackages(rootDir)) {
2050
+ const result = checkAdapterPackage(workspace, targetById);
2051
+ diagnostics.push(...result.diagnostics);
2052
+ if (result.subject) {
2053
+ subjects.push(result.subject);
2054
+ }
2055
+ }
2056
+
2057
+ return {
2058
+ diagnostics,
2059
+ subjects: subjects.toSorted((left, right) =>
2060
+ left.key.localeCompare(right.key)
2061
+ ),
2062
+ targets: catalog.targets,
2063
+ };
2064
+ };