@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/catalog.ts ADDED
@@ -0,0 +1,1449 @@
1
+ /**
2
+ * Read-only adapter target catalog derivation.
3
+ *
4
+ * Owner packages author the few adapter facts package metadata cannot derive.
5
+ * Tooling consumes those facts; runtime adapters must never import this
6
+ * internal tooling package.
7
+ */
8
+
9
+ import {
10
+ existsSync,
11
+ readdirSync,
12
+ readFileSync,
13
+ realpathSync,
14
+ statSync,
15
+ } from 'node:fs';
16
+ import { dirname, join, resolve } from 'node:path';
17
+
18
+ export const adapterTargetPlacements = ['extracted', 'subpath'] as const;
19
+
20
+ export type AdapterTargetPlacementValue =
21
+ (typeof adapterTargetPlacements)[number];
22
+
23
+ export type AdapterTargetPlacement = AdapterTargetPlacementValue;
24
+
25
+ export interface AdapterTargetConformanceManifest {
26
+ readonly adapterType: string;
27
+ readonly casesFactory: string;
28
+ readonly runner: string;
29
+ }
30
+
31
+ export interface AdapterTargetManifestEntry {
32
+ readonly conformance?: AdapterTargetConformanceManifest | undefined;
33
+ readonly placements: readonly AdapterTargetPlacement[];
34
+ readonly supportImport?: string | undefined;
35
+ readonly testingImport?: string | undefined;
36
+ }
37
+
38
+ export interface AdapterTargetCatalogEntry extends AdapterTargetManifestEntry {
39
+ readonly key: string;
40
+ readonly ownerPackage: string;
41
+ readonly packageJsonPath: string;
42
+ readonly packageRoot: string;
43
+ readonly supportExportTarget?: string | undefined;
44
+ readonly target: string;
45
+ readonly testingExportTarget?: string | undefined;
46
+ }
47
+
48
+ export type AdapterTargetCatalogDiagnosticCode =
49
+ | 'duplicate-adapter-target'
50
+ | 'invalid-adapter-target'
51
+ | 'invalid-adapter-targets'
52
+ | 'invalid-conformance'
53
+ | 'invalid-import'
54
+ | 'invalid-placement';
55
+
56
+ export interface AdapterTargetCatalogDiagnostic {
57
+ readonly code: AdapterTargetCatalogDiagnosticCode;
58
+ readonly message: string;
59
+ readonly packageJsonPath: string;
60
+ readonly packageName?: string | undefined;
61
+ readonly target?: string | undefined;
62
+ }
63
+
64
+ export interface AdapterTargetCatalog {
65
+ readonly diagnostics: readonly AdapterTargetCatalogDiagnostic[];
66
+ readonly targets: readonly AdapterTargetCatalogEntry[];
67
+ }
68
+
69
+ interface RootManifest {
70
+ readonly workspaces?: unknown;
71
+ }
72
+
73
+ export interface AdapterTargetPackageManifest {
74
+ readonly exports?: unknown;
75
+ readonly name?: unknown;
76
+ readonly trails?: unknown;
77
+ }
78
+
79
+ export interface AdapterTargetParseContext {
80
+ readonly blockedExportSpecifiers: readonly string[];
81
+ readonly exportTargets: Readonly<Record<string, string>>;
82
+ readonly packageJsonPath: string;
83
+ readonly packageName: string;
84
+ readonly packageRoot: string;
85
+ }
86
+
87
+ interface ParsedCatalogTarget {
88
+ readonly diagnostics: readonly AdapterTargetCatalogDiagnostic[];
89
+ readonly targetEntry?: AdapterTargetCatalogEntry | undefined;
90
+ }
91
+
92
+ type ExportKind = 'type' | 'type-value' | 'value';
93
+
94
+ interface StarExportSpecifier {
95
+ readonly specifier: string;
96
+ readonly typeOnly: boolean;
97
+ }
98
+
99
+ interface NamedExportSpecifier {
100
+ readonly identifier: string;
101
+ readonly specifier: string;
102
+ readonly typeOnly: boolean;
103
+ }
104
+
105
+ interface NamedImportSpecifier {
106
+ readonly identifier: string;
107
+ readonly specifier: string;
108
+ readonly typeOnly: boolean;
109
+ }
110
+
111
+ interface ExportListSpecifier {
112
+ readonly exported: string;
113
+ readonly local: string;
114
+ readonly typeOnly: boolean;
115
+ }
116
+
117
+ type ImportKindResolver = (
118
+ importSpecifier: NamedImportSpecifier
119
+ ) => ExportKind | undefined;
120
+
121
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
122
+ Boolean(value && typeof value === 'object' && !Array.isArray(value));
123
+
124
+ const targetIdPattern = /^[a-z][a-z0-9-]*$/u;
125
+ const exportIdentifierPattern = /^[A-Za-z_$][\w$]*$/u;
126
+
127
+ const exportKindHasType = (kind: ExportKind | undefined): boolean =>
128
+ kind === 'type' || kind === 'type-value';
129
+
130
+ const exportKindHasValue = (kind: ExportKind | undefined): boolean =>
131
+ kind === 'value' || kind === 'type-value';
132
+
133
+ const normalizePath = (path: string): string => path.replaceAll('\\', '/');
134
+
135
+ const normalizeRealPath = (path: string): string => {
136
+ try {
137
+ return normalizePath(realpathSync(path));
138
+ } catch {
139
+ return normalizePath(resolve(path));
140
+ }
141
+ };
142
+
143
+ const pathIsFile = (path: string): boolean => {
144
+ try {
145
+ return statSync(path).isFile();
146
+ } catch {
147
+ return false;
148
+ }
149
+ };
150
+
151
+ const localDeclarationKind = (
152
+ code: string,
153
+ identifier: string,
154
+ exportedOnly = false
155
+ ): ExportKind | undefined => {
156
+ const escapedIdentifier = identifier.replaceAll(
157
+ /[.*+?^${}()|[\]\\]/gu,
158
+ '\\$&'
159
+ );
160
+ const exportPrefix = exportedOnly ? '\\bexport\\s+' : '\\b(?:export\\s+)?';
161
+ const valueDeclarationPattern = new RegExp(
162
+ `${exportPrefix}(?!declare\\s+)(?:(?:async\\s+)?function|const|let|var)\\s+${escapedIdentifier}\\b`,
163
+ 'u'
164
+ );
165
+ if (valueDeclarationPattern.test(code)) {
166
+ return 'value';
167
+ }
168
+
169
+ const typeValueDeclarationPattern = new RegExp(
170
+ `${exportPrefix}(?!declare\\s+)(?:abstract\\s+)?(?:class|enum)\\s+${escapedIdentifier}\\b`,
171
+ 'u'
172
+ );
173
+ if (typeValueDeclarationPattern.test(code)) {
174
+ return 'type-value';
175
+ }
176
+
177
+ const typeDeclarationPattern = new RegExp(
178
+ `${exportPrefix}(?:declare\\s+)?(?:interface|type)\\s+${escapedIdentifier}\\b`,
179
+ 'u'
180
+ );
181
+ return typeDeclarationPattern.test(code) ? 'type' : undefined;
182
+ };
183
+
184
+ const readJson = <T>(path: string): T | undefined => {
185
+ try {
186
+ return JSON.parse(readFileSync(path, 'utf8')) as T;
187
+ } catch {
188
+ return undefined;
189
+ }
190
+ };
191
+
192
+ const maskDeadSourceText = (
193
+ source: string,
194
+ options: { strings: boolean }
195
+ ): string => {
196
+ const output = [...source];
197
+ let index = 0;
198
+
199
+ const maskRange = (start: number, end: number): void => {
200
+ for (let cursor = start; cursor < end; cursor += 1) {
201
+ if (output[cursor] !== '\n') {
202
+ output[cursor] = ' ';
203
+ }
204
+ }
205
+ };
206
+
207
+ const skipQuoted = (quote: '"' | "'" | '`'): void => {
208
+ const start = index;
209
+ index += 1;
210
+ while (index < source.length) {
211
+ if (source[index] === '\\') {
212
+ index += 2;
213
+ continue;
214
+ }
215
+ if (source[index] === quote) {
216
+ index += 1;
217
+ break;
218
+ }
219
+ index += 1;
220
+ }
221
+ if (options.strings) {
222
+ maskRange(start, index);
223
+ }
224
+ };
225
+
226
+ while (index < source.length) {
227
+ if (source.startsWith('//', index)) {
228
+ const end = source.indexOf('\n', index + 2);
229
+ const stop = end === -1 ? source.length : end;
230
+ maskRange(index, stop);
231
+ index = stop;
232
+ continue;
233
+ }
234
+ if (source.startsWith('/*', index)) {
235
+ const end = source.indexOf('*/', index + 2);
236
+ const stop = end === -1 ? source.length : end + 2;
237
+ maskRange(index, stop);
238
+ index = stop;
239
+ continue;
240
+ }
241
+ const char = source[index];
242
+ if (char === '"' || char === "'" || char === '`') {
243
+ skipQuoted(char);
244
+ continue;
245
+ }
246
+ index += 1;
247
+ }
248
+
249
+ return output.join('');
250
+ };
251
+
252
+ const defaultExportKind = (source: string): ExportKind | undefined => {
253
+ const code = maskDeadSourceText(source, { strings: true });
254
+ if (/\bexport\s+default\s+(?:async\s+)?function\*?\b/u.test(code)) {
255
+ return 'value';
256
+ }
257
+ if (/\bexport\s+default\s+(?:abstract\s+)?(?:class|enum)\b/u.test(code)) {
258
+ return 'type-value';
259
+ }
260
+ if (/\bexport\s+default\s+(?:interface|type)\b/u.test(code)) {
261
+ return 'type';
262
+ }
263
+
264
+ const expressionIdentifier =
265
+ /\bexport\s+default\s+(?<identifier>[A-Za-z_$][\w$]*)\b/u.exec(code)
266
+ ?.groups?.['identifier'];
267
+ if (expressionIdentifier) {
268
+ return localDeclarationKind(code, expressionIdentifier) ?? 'value';
269
+ }
270
+
271
+ return /\bexport\s+default\b/u.test(code) ? 'value' : undefined;
272
+ };
273
+
274
+ const localDefaultImportSpecifier = (
275
+ code: string,
276
+ stringsMaskedCode: string,
277
+ identifier: string
278
+ ): NamedImportSpecifier | undefined => {
279
+ const pattern =
280
+ /\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;
281
+
282
+ for (const match of code.matchAll(pattern)) {
283
+ if (!stringsMaskedCode.startsWith('import', match.index ?? 0)) {
284
+ continue;
285
+ }
286
+ if (match.groups?.['local'] !== identifier) {
287
+ continue;
288
+ }
289
+
290
+ return {
291
+ identifier: 'default',
292
+ specifier: match.groups?.['specifier'] ?? '',
293
+ typeOnly: Boolean(match.groups?.['importTypeOnly']),
294
+ };
295
+ }
296
+
297
+ return undefined;
298
+ };
299
+
300
+ const parseNamedImportSpecifier = (
301
+ item: string,
302
+ declarationTypeOnly: boolean,
303
+ specifier: string,
304
+ identifier: string
305
+ ): NamedImportSpecifier | undefined => {
306
+ const trimmedItem = item.trim();
307
+ if (!trimmedItem) {
308
+ return undefined;
309
+ }
310
+
311
+ const itemTypeOnly = declarationTypeOnly || trimmedItem.startsWith('type ');
312
+ const specifierText = trimmedItem.replace(/^type\s+/u, '');
313
+ const imported =
314
+ /^(?<imported>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<local>[A-Za-z_$][\w$]*))?$/u.exec(
315
+ specifierText
316
+ )?.groups;
317
+ const local = imported?.['local'] ?? imported?.['imported'];
318
+ const importedName = imported?.['imported'];
319
+ if (local !== identifier || !importedName) {
320
+ return undefined;
321
+ }
322
+
323
+ return {
324
+ identifier: importedName,
325
+ specifier,
326
+ typeOnly: itemTypeOnly,
327
+ };
328
+ };
329
+
330
+ const localNamedImportSpecifier = (
331
+ source: string,
332
+ identifier: string
333
+ ): NamedImportSpecifier | undefined => {
334
+ const code = maskDeadSourceText(source, { strings: false });
335
+ const stringsMaskedCode = maskDeadSourceText(source, { strings: true });
336
+ const defaultSpecifier = localDefaultImportSpecifier(
337
+ code,
338
+ stringsMaskedCode,
339
+ identifier
340
+ );
341
+ if (defaultSpecifier) {
342
+ return defaultSpecifier;
343
+ }
344
+
345
+ const pattern =
346
+ /\bimport\s+(?<importTypeOnly>type\s+)?\{(?<imports>[\s\S]*?)\}\s+from\s+['"](?<specifier>[^'"]+)['"]/gu;
347
+
348
+ for (const match of code.matchAll(pattern)) {
349
+ if (!stringsMaskedCode.startsWith('import', match.index ?? 0)) {
350
+ continue;
351
+ }
352
+
353
+ const declarationTypeOnly = Boolean(match.groups?.['importTypeOnly']);
354
+ const namedImports = match.groups?.['imports'] ?? '';
355
+ const specifier = match.groups?.['specifier'] ?? '';
356
+ for (const item of namedImports.split(',')) {
357
+ const namedSpecifier = parseNamedImportSpecifier(
358
+ item,
359
+ declarationTypeOnly,
360
+ specifier,
361
+ identifier
362
+ );
363
+ if (namedSpecifier) {
364
+ return namedSpecifier;
365
+ }
366
+ }
367
+ }
368
+
369
+ return undefined;
370
+ };
371
+
372
+ const parseExportListSpecifier = (
373
+ item: string,
374
+ declarationTypeOnly: boolean
375
+ ): ExportListSpecifier | undefined => {
376
+ const trimmedItem = item.trim();
377
+ if (!trimmedItem) {
378
+ return undefined;
379
+ }
380
+
381
+ const typeOnly = declarationTypeOnly || trimmedItem.startsWith('type ');
382
+ const specifierText = trimmedItem.replace(/^type\s+/u, '');
383
+ const exported =
384
+ /^(?<local>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<name>[A-Za-z_$][\w$]*))?$/u.exec(
385
+ specifierText
386
+ )?.groups;
387
+ const local = exported?.['local'];
388
+ if (!local) {
389
+ return undefined;
390
+ }
391
+
392
+ return {
393
+ exported: exported?.['name'] ?? local,
394
+ local,
395
+ typeOnly,
396
+ };
397
+ };
398
+
399
+ const exportedLocalBindingKind = (
400
+ source: string,
401
+ code: string,
402
+ local: string,
403
+ resolveImportKind: ImportKindResolver
404
+ ): ExportKind | undefined => {
405
+ const localKind = localDeclarationKind(code, local);
406
+ if (localKind) {
407
+ return localKind;
408
+ }
409
+
410
+ const importSpecifier = localNamedImportSpecifier(source, local);
411
+ if (importSpecifier?.typeOnly) {
412
+ return 'type';
413
+ }
414
+ return importSpecifier ? resolveImportKind(importSpecifier) : undefined;
415
+ };
416
+
417
+ const workspacePatternsFromManifest = (
418
+ manifest: RootManifest | undefined
419
+ ): readonly string[] => {
420
+ const { workspaces } = manifest ?? {};
421
+ if (Array.isArray(workspaces)) {
422
+ return workspaces.filter(
423
+ (pattern): pattern is string => typeof pattern === 'string'
424
+ );
425
+ }
426
+
427
+ const packages = isRecord(workspaces) ? workspaces['packages'] : undefined;
428
+ return Array.isArray(packages)
429
+ ? packages.filter(
430
+ (pattern): pattern is string => typeof pattern === 'string'
431
+ )
432
+ : [];
433
+ };
434
+
435
+ const workspaceDirsForPattern = (
436
+ rootDir: string,
437
+ pattern: string
438
+ ): readonly string[] => {
439
+ if (!pattern.endsWith('/*')) {
440
+ const workspaceDir = join(rootDir, pattern);
441
+ return existsSync(workspaceDir) ? [workspaceDir] : [];
442
+ }
443
+
444
+ const groupDir = join(rootDir, pattern.slice(0, -2));
445
+ if (!existsSync(groupDir)) {
446
+ return [];
447
+ }
448
+
449
+ return readdirSync(groupDir, { withFileTypes: true })
450
+ .filter((entry) => entry.isDirectory())
451
+ .map((entry) => join(groupDir, entry.name))
452
+ .toSorted();
453
+ };
454
+
455
+ const exportConditions = new Set([
456
+ 'bun',
457
+ 'node',
458
+ 'node-addons',
459
+ 'module-sync',
460
+ 'import',
461
+ 'default',
462
+ ]);
463
+
464
+ type ResolvedExportTarget =
465
+ | { readonly kind: 'target'; readonly target: string }
466
+ | { readonly kind: 'blocked' };
467
+
468
+ const packageExportSegmentIsSafe = (segment: string): boolean => {
469
+ if (segment.length === 0) {
470
+ return false;
471
+ }
472
+ let decoded: string;
473
+ try {
474
+ decoded = decodeURIComponent(segment);
475
+ } catch {
476
+ return false;
477
+ }
478
+ return (
479
+ decoded !== '.' &&
480
+ decoded !== '..' &&
481
+ decoded.toLowerCase() !== 'node_modules' &&
482
+ !decoded.includes('/') &&
483
+ !decoded.includes('\\')
484
+ );
485
+ };
486
+
487
+ const exportTargetIsSafe = (target: string): boolean =>
488
+ target.startsWith('./') &&
489
+ !target.includes('\\') &&
490
+ target.slice(2).split('/').every(packageExportSegmentIsSafe);
491
+
492
+ const resolveExportTarget = (
493
+ target: unknown,
494
+ depth = 0
495
+ ): ResolvedExportTarget | undefined => {
496
+ if (typeof target === 'string') {
497
+ return { kind: 'target', target };
498
+ }
499
+ if (target === null) {
500
+ return { kind: 'blocked' };
501
+ }
502
+ if (Array.isArray(target)) {
503
+ if (depth > 8) {
504
+ return undefined;
505
+ }
506
+ for (const targetEntry of target) {
507
+ const resolvedTarget = resolveExportTarget(targetEntry, depth + 1);
508
+ if (
509
+ resolvedTarget?.kind === 'target' &&
510
+ exportTargetIsSafe(resolvedTarget.target)
511
+ ) {
512
+ return resolvedTarget;
513
+ }
514
+ }
515
+ return { kind: 'blocked' };
516
+ }
517
+ if (!isRecord(target) || depth > 8) {
518
+ return undefined;
519
+ }
520
+
521
+ for (const [condition, conditionTarget] of Object.entries(target)) {
522
+ if (!exportConditions.has(condition)) {
523
+ continue;
524
+ }
525
+ const resolvedTarget = resolveExportTarget(conditionTarget, depth + 1);
526
+ if (resolvedTarget) {
527
+ return resolvedTarget;
528
+ }
529
+ }
530
+ return undefined;
531
+ };
532
+
533
+ const exportSpecifierFromKey = (
534
+ packageName: string,
535
+ key: string
536
+ ): string | undefined => {
537
+ if (key === '.') {
538
+ return packageName;
539
+ }
540
+ if (!key.startsWith('./')) {
541
+ return undefined;
542
+ }
543
+ return `${packageName}/${key.slice(2)}`;
544
+ };
545
+
546
+ const wildcardCaptureIsSafe = (capture: string): boolean =>
547
+ !capture.includes('\\') &&
548
+ capture.split('/').every(packageExportSegmentIsSafe);
549
+
550
+ const wildcardCapture = (
551
+ pattern: string,
552
+ value: string
553
+ ): string | undefined => {
554
+ const star = pattern.indexOf('*');
555
+ if (star === -1) {
556
+ return undefined;
557
+ }
558
+
559
+ const prefix = pattern.slice(0, star);
560
+ const suffix = pattern.slice(star + 1);
561
+ if (!value.startsWith(prefix) || !value.endsWith(suffix)) {
562
+ return undefined;
563
+ }
564
+
565
+ const capture = value.slice(prefix.length, value.length - suffix.length);
566
+ return capture.length > 0 && wildcardCaptureIsSafe(capture)
567
+ ? capture
568
+ : undefined;
569
+ };
570
+
571
+ const applyWildcardCapture = (targetPattern: string, capture: string): string =>
572
+ targetPattern.replaceAll('*', capture);
573
+
574
+ type WildcardExportCandidate =
575
+ | {
576
+ readonly kind: 'target';
577
+ readonly pattern: string;
578
+ readonly target: string;
579
+ }
580
+ | { readonly kind: 'blocked'; readonly pattern: string };
581
+
582
+ /**
583
+ * Order two wildcard export keys by Node's package-exports precedence: the
584
+ * longer prefix before the wildcard wins first, then the longer total key. This
585
+ * mirrors Node's `patternKeyCompare`, so equal-total-length patterns (for
586
+ * example a leading-wildcard key versus a trailing-wildcard key) resolve the
587
+ * way the runtime loader would.
588
+ */
589
+ const patternKeyCompare = (left: string, right: string): number => {
590
+ const leftBase = left.indexOf('*') + 1;
591
+ const rightBase = right.indexOf('*') + 1;
592
+ if (leftBase !== rightBase) {
593
+ return rightBase - leftBase;
594
+ }
595
+ return right.length - left.length;
596
+ };
597
+
598
+ const resolveExportTargetForImport = (
599
+ context: AdapterTargetParseContext,
600
+ importSpecifier: string
601
+ ): string | undefined => {
602
+ // Exact `null` exclusions block the subpath before any wildcard fallback.
603
+ if (context.blockedExportSpecifiers.includes(importSpecifier)) {
604
+ return undefined;
605
+ }
606
+
607
+ const exactTarget = context.exportTargets[importSpecifier];
608
+ if (exactTarget) {
609
+ return exactTarget;
610
+ }
611
+
612
+ // Match the most specific wildcard key using Node's exports precedence
613
+ // (longer prefix before the wildcard first, then longer total key). A blocked
614
+ // pattern that is more specific than a broader target pattern must reject the
615
+ // import instead of resolving it.
616
+ const candidates: WildcardExportCandidate[] = [
617
+ ...Object.entries(context.exportTargets)
618
+ .filter(([specifier]) => specifier.includes('*'))
619
+ .map(
620
+ ([pattern, target]): WildcardExportCandidate => ({
621
+ kind: 'target',
622
+ pattern,
623
+ target,
624
+ })
625
+ ),
626
+ ...context.blockedExportSpecifiers
627
+ .filter((specifier) => specifier.includes('*'))
628
+ .map(
629
+ (pattern): WildcardExportCandidate => ({ kind: 'blocked', pattern })
630
+ ),
631
+ ].toSorted((left, right) => patternKeyCompare(left.pattern, right.pattern));
632
+
633
+ for (const candidate of candidates) {
634
+ const capture = wildcardCapture(candidate.pattern, importSpecifier);
635
+ if (capture === undefined) {
636
+ continue;
637
+ }
638
+ return candidate.kind === 'blocked'
639
+ ? undefined
640
+ : applyWildcardCapture(candidate.target, capture);
641
+ }
642
+
643
+ return undefined;
644
+ };
645
+
646
+ interface NormalizedExportTargets {
647
+ readonly blocked: readonly string[];
648
+ readonly targets: Readonly<Record<string, string>>;
649
+ }
650
+
651
+ const normalizeExportTargets = (
652
+ packageRoot: string,
653
+ packageName: string,
654
+ exportsValue: unknown
655
+ ): NormalizedExportTargets => {
656
+ if (!isRecord(exportsValue)) {
657
+ return { blocked: [], targets: {} };
658
+ }
659
+
660
+ const targets: Record<string, string> = {};
661
+ const blocked: string[] = [];
662
+ for (const [key, value] of Object.entries(exportsValue)) {
663
+ const specifier = exportSpecifierFromKey(packageName, key);
664
+ if (!specifier) {
665
+ continue;
666
+ }
667
+ const resolvedTarget = resolveExportTarget(value);
668
+ if (resolvedTarget?.kind === 'target') {
669
+ if (!exportTargetIsSafe(resolvedTarget.target)) {
670
+ blocked.push(specifier);
671
+ continue;
672
+ }
673
+ targets[specifier] = normalizeRealPath(
674
+ join(packageRoot, resolvedTarget.target)
675
+ );
676
+ continue;
677
+ }
678
+ // A declared export key that does not resolve to a runtime target (an
679
+ // explicit `null` exclusion, or a conditions object with no runtime
680
+ // condition such as a `types`-only entry) blocks the subpath. Node selects
681
+ // the most specific matching key and reports the subpath as not exported, so
682
+ // it must not fall through to a broader wildcard.
683
+ blocked.push(specifier);
684
+ }
685
+ return { blocked, targets };
686
+ };
687
+
688
+ const diagnostic = (
689
+ context: AdapterTargetParseContext,
690
+ code: AdapterTargetCatalogDiagnosticCode,
691
+ message: string,
692
+ target?: string
693
+ ): AdapterTargetCatalogDiagnostic => ({
694
+ code,
695
+ message,
696
+ packageJsonPath: context.packageJsonPath,
697
+ packageName: context.packageName,
698
+ ...(target === undefined ? {} : { target }),
699
+ });
700
+
701
+ const targetDiagnostic = (
702
+ entry: AdapterTargetCatalogEntry,
703
+ code: AdapterTargetCatalogDiagnosticCode,
704
+ message: string
705
+ ): AdapterTargetCatalogDiagnostic => ({
706
+ code,
707
+ message,
708
+ packageJsonPath: entry.packageJsonPath,
709
+ packageName: entry.ownerPackage,
710
+ target: entry.target,
711
+ });
712
+
713
+ const rejectDuplicateTargetIds = (
714
+ targets: readonly AdapterTargetCatalogEntry[]
715
+ ): {
716
+ readonly diagnostics: readonly AdapterTargetCatalogDiagnostic[];
717
+ readonly targets: readonly AdapterTargetCatalogEntry[];
718
+ } => {
719
+ const entriesByTarget = new Map<string, AdapterTargetCatalogEntry[]>();
720
+ for (const entry of targets) {
721
+ entriesByTarget.set(entry.target, [
722
+ ...(entriesByTarget.get(entry.target) ?? []),
723
+ entry,
724
+ ]);
725
+ }
726
+
727
+ const duplicateTargets = new Set(
728
+ [...entriesByTarget.entries()]
729
+ .filter(([, entries]) => entries.length > 1)
730
+ .map(([target]) => target)
731
+ );
732
+
733
+ return {
734
+ diagnostics: [...entriesByTarget.values()]
735
+ .filter((entries) => entries.length > 1)
736
+ .flatMap((entries) =>
737
+ entries.map((entry) =>
738
+ targetDiagnostic(
739
+ entry,
740
+ 'duplicate-adapter-target',
741
+ `Adapter target "${entry.target}" is declared by multiple owner packages; target ids must be globally unique until adapter metadata can select an owner.`
742
+ )
743
+ )
744
+ ),
745
+ targets: targets.filter((entry) => !duplicateTargets.has(entry.target)),
746
+ };
747
+ };
748
+
749
+ const isAdapterTargetPlacement = (
750
+ value: unknown
751
+ ): value is AdapterTargetPlacement =>
752
+ typeof value === 'string' &&
753
+ adapterTargetPlacements.includes(value as AdapterTargetPlacement);
754
+
755
+ const normalizePlacements = (
756
+ value: unknown,
757
+ context: AdapterTargetParseContext,
758
+ target: string
759
+ ): {
760
+ readonly diagnostics: readonly AdapterTargetCatalogDiagnostic[];
761
+ readonly placements: readonly AdapterTargetPlacement[];
762
+ } => {
763
+ if (!Array.isArray(value)) {
764
+ return {
765
+ diagnostics: [
766
+ diagnostic(
767
+ context,
768
+ 'invalid-placement',
769
+ `Adapter target "${target}" must declare placements as an array.`,
770
+ target
771
+ ),
772
+ ],
773
+ placements: [],
774
+ };
775
+ }
776
+
777
+ const diagnostics: AdapterTargetCatalogDiagnostic[] = [];
778
+ if (value.length === 0) {
779
+ diagnostics.push(
780
+ diagnostic(
781
+ context,
782
+ 'invalid-placement',
783
+ `Adapter target "${target}" must declare at least one placement.`,
784
+ target
785
+ )
786
+ );
787
+ }
788
+
789
+ const placements = new Set<AdapterTargetPlacement>();
790
+ for (const placement of value) {
791
+ if (isAdapterTargetPlacement(placement)) {
792
+ placements.add(placement);
793
+ continue;
794
+ }
795
+ diagnostics.push(
796
+ diagnostic(
797
+ context,
798
+ 'invalid-placement',
799
+ `Adapter target "${target}" has unsupported placement ${JSON.stringify(placement)}.`,
800
+ target
801
+ )
802
+ );
803
+ }
804
+
805
+ return {
806
+ diagnostics,
807
+ placements: [...placements].toSorted(),
808
+ };
809
+ };
810
+
811
+ const normalizeOptionalImport = (
812
+ value: unknown,
813
+ field: 'supportImport' | 'testingImport',
814
+ context: AdapterTargetParseContext,
815
+ target: string
816
+ ): {
817
+ readonly diagnostics: readonly AdapterTargetCatalogDiagnostic[];
818
+ readonly importSpecifier?: string | undefined;
819
+ } => {
820
+ if (value === undefined) {
821
+ return { diagnostics: [] };
822
+ }
823
+ if (typeof value === 'string' && value.length > 0) {
824
+ if (!value.startsWith(`${context.packageName}/`)) {
825
+ return {
826
+ diagnostics: [
827
+ diagnostic(
828
+ context,
829
+ 'invalid-import',
830
+ `Adapter target "${target}" must declare ${field} as an owner package subpath inside ${context.packageName}.`,
831
+ target
832
+ ),
833
+ ],
834
+ };
835
+ }
836
+
837
+ return { diagnostics: [], importSpecifier: value };
838
+ }
839
+ return {
840
+ diagnostics: [
841
+ diagnostic(
842
+ context,
843
+ 'invalid-import',
844
+ `Adapter target "${target}" must declare ${field} as a non-empty string when present.`,
845
+ target
846
+ ),
847
+ ],
848
+ };
849
+ };
850
+
851
+ const normalizeConformance = (
852
+ value: unknown,
853
+ context: AdapterTargetParseContext,
854
+ target: string,
855
+ hasTestingImport: boolean
856
+ ): {
857
+ readonly diagnostics: readonly AdapterTargetCatalogDiagnostic[];
858
+ readonly conformance?: AdapterTargetConformanceManifest | undefined;
859
+ } => {
860
+ if (value === undefined) {
861
+ return { diagnostics: [] };
862
+ }
863
+
864
+ if (!isRecord(value)) {
865
+ return {
866
+ diagnostics: [
867
+ diagnostic(
868
+ context,
869
+ 'invalid-conformance',
870
+ `Adapter target "${target}" must declare conformance as an object when present.`,
871
+ target
872
+ ),
873
+ ],
874
+ };
875
+ }
876
+
877
+ const diagnostics: AdapterTargetCatalogDiagnostic[] = [];
878
+ if (!hasTestingImport) {
879
+ diagnostics.push(
880
+ diagnostic(
881
+ context,
882
+ 'invalid-conformance',
883
+ `Adapter target "${target}" must declare testingImport before conformance helpers.`,
884
+ target
885
+ )
886
+ );
887
+ }
888
+
889
+ const conformance: {
890
+ adapterType?: string | undefined;
891
+ casesFactory?: string | undefined;
892
+ runner?: string | undefined;
893
+ } = {};
894
+ for (const field of ['adapterType', 'casesFactory', 'runner'] as const) {
895
+ const fieldValue = value[field];
896
+ if (
897
+ typeof fieldValue === 'string' &&
898
+ exportIdentifierPattern.test(fieldValue)
899
+ ) {
900
+ conformance[field] = fieldValue;
901
+ continue;
902
+ }
903
+ diagnostics.push(
904
+ diagnostic(
905
+ context,
906
+ 'invalid-conformance',
907
+ `Adapter target "${target}" must declare conformance.${field} as a valid named export.`,
908
+ target
909
+ )
910
+ );
911
+ }
912
+
913
+ if (diagnostics.length > 0) {
914
+ return { diagnostics };
915
+ }
916
+
917
+ return {
918
+ conformance: conformance as AdapterTargetConformanceManifest,
919
+ diagnostics: [],
920
+ };
921
+ };
922
+
923
+ const namedExportKind = (
924
+ source: string,
925
+ identifier: string,
926
+ resolveImportKind: ImportKindResolver
927
+ ): ExportKind | undefined => {
928
+ if (identifier === 'default') {
929
+ const defaultKind = defaultExportKind(source);
930
+ if (defaultKind) {
931
+ return defaultKind;
932
+ }
933
+ }
934
+
935
+ const code = maskDeadSourceText(source, { strings: true });
936
+ const directKind = localDeclarationKind(code, identifier, true);
937
+ if (directKind) {
938
+ return directKind;
939
+ }
940
+
941
+ const exportListPattern =
942
+ /\bexport\s+(?<typeOnly>type\s+)?\{(?<exports>[\s\S]*?)\}(?!\s+from\b)/gu;
943
+
944
+ for (const match of code.matchAll(exportListPattern)) {
945
+ const declarationTypeOnly = Boolean(match.groups?.['typeOnly']);
946
+ const namedExports = match.groups?.['exports'] ?? '';
947
+ for (const item of namedExports.split(',')) {
948
+ const exported = parseExportListSpecifier(item, declarationTypeOnly);
949
+ if (exported?.exported !== identifier) {
950
+ continue;
951
+ }
952
+
953
+ const localKind = exportedLocalBindingKind(
954
+ source,
955
+ code,
956
+ exported.local,
957
+ resolveImportKind
958
+ );
959
+ if (!localKind) {
960
+ return undefined;
961
+ }
962
+ return exported.typeOnly ? 'type' : localKind;
963
+ }
964
+ }
965
+
966
+ return undefined;
967
+ };
968
+
969
+ const starExportSpecifiers = (
970
+ source: string
971
+ ): readonly StarExportSpecifier[] => {
972
+ const code = maskDeadSourceText(source, { strings: false });
973
+ const stringsMaskedCode = maskDeadSourceText(source, { strings: true });
974
+ return [
975
+ ...code.matchAll(
976
+ /\bexport\s+(?<typeOnly>type\s+)?\*\s+from\s+['"](?<specifier>[^'"]+)['"]/gu
977
+ ),
978
+ ]
979
+ .filter((match) => stringsMaskedCode.startsWith('export', match.index ?? 0))
980
+ .map((match) => ({
981
+ specifier: match.groups?.['specifier'] ?? '',
982
+ typeOnly: Boolean(match.groups?.['typeOnly']),
983
+ }));
984
+ };
985
+
986
+ const namedExportSpecifiers = (
987
+ source: string,
988
+ identifier: string
989
+ ): readonly NamedExportSpecifier[] => {
990
+ const code = maskDeadSourceText(source, { strings: false });
991
+ const stringsMaskedCode = maskDeadSourceText(source, { strings: true });
992
+ const exports: NamedExportSpecifier[] = [];
993
+ const pattern =
994
+ /\bexport\s+(?<typeOnly>type\s+)?\{(?<exports>[\s\S]*?)\}\s+from\s+['"](?<specifier>[^'"]+)['"]/gu;
995
+
996
+ for (const match of code.matchAll(pattern)) {
997
+ if (!stringsMaskedCode.startsWith('export', match.index ?? 0)) {
998
+ continue;
999
+ }
1000
+
1001
+ const specifier = match.groups?.['specifier'];
1002
+ if (!specifier?.startsWith('.')) {
1003
+ continue;
1004
+ }
1005
+
1006
+ const declarationTypeOnly = Boolean(match.groups?.['typeOnly']);
1007
+ const namedExports = match.groups?.['exports'] ?? '';
1008
+ for (const item of namedExports.split(',')) {
1009
+ const trimmedItem = item.trim();
1010
+ if (!trimmedItem) {
1011
+ continue;
1012
+ }
1013
+
1014
+ const itemTypeOnly =
1015
+ declarationTypeOnly || trimmedItem.startsWith('type ');
1016
+ const specifierText = trimmedItem.replace(/^type\s+/u, '');
1017
+ const exported =
1018
+ /^(?<local>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<name>[A-Za-z_$][\w$]*))?$/u.exec(
1019
+ specifierText
1020
+ )?.groups;
1021
+ const local = exported?.['local'];
1022
+ if (!local || (exported?.['name'] ?? local) !== identifier) {
1023
+ continue;
1024
+ }
1025
+
1026
+ exports.push({
1027
+ identifier: local,
1028
+ specifier,
1029
+ typeOnly: itemTypeOnly,
1030
+ });
1031
+ }
1032
+ }
1033
+
1034
+ return exports;
1035
+ };
1036
+
1037
+ const resolveLocalModuleSpecifier = (
1038
+ sourcePath: string,
1039
+ specifier: string
1040
+ ): string | undefined => {
1041
+ if (!specifier.startsWith('.')) {
1042
+ return undefined;
1043
+ }
1044
+
1045
+ const basePath = resolve(dirname(sourcePath), specifier);
1046
+ const candidates = [
1047
+ basePath,
1048
+ basePath.endsWith('.js') ? `${basePath.slice(0, -3)}.ts` : undefined,
1049
+ basePath.endsWith('.js') ? `${basePath.slice(0, -3)}.tsx` : undefined,
1050
+ basePath.endsWith('.mjs') ? `${basePath.slice(0, -4)}.mts` : undefined,
1051
+ `${basePath}.ts`,
1052
+ `${basePath}.tsx`,
1053
+ join(basePath, 'index.ts'),
1054
+ join(basePath, 'index.tsx'),
1055
+ ].filter((candidate): candidate is string => candidate !== undefined);
1056
+
1057
+ return candidates.find((candidate) => existsSync(candidate));
1058
+ };
1059
+
1060
+ const namedExportKindFromFile = (
1061
+ sourcePath: string,
1062
+ identifier: string,
1063
+ visited = new Set<string>()
1064
+ ): ExportKind | undefined => {
1065
+ const normalizedSourcePath = normalizeRealPath(sourcePath);
1066
+ const visitKey = `${normalizedSourcePath}:${identifier}`;
1067
+ if (visited.has(visitKey)) {
1068
+ return undefined;
1069
+ }
1070
+ visited.add(visitKey);
1071
+
1072
+ let source: string;
1073
+ try {
1074
+ source = readFileSync(normalizedSourcePath, 'utf8');
1075
+ } catch {
1076
+ return undefined;
1077
+ }
1078
+
1079
+ const directKind = namedExportKind(source, identifier, (importSpecifier) => {
1080
+ const importTarget = resolveLocalModuleSpecifier(
1081
+ normalizedSourcePath,
1082
+ importSpecifier.specifier
1083
+ );
1084
+ if (!importTarget) {
1085
+ return;
1086
+ }
1087
+ return namedExportKindFromFile(
1088
+ importTarget,
1089
+ importSpecifier.identifier,
1090
+ new Set(visited)
1091
+ );
1092
+ });
1093
+ if (directKind) {
1094
+ return directKind;
1095
+ }
1096
+
1097
+ let sawTypeExport = false;
1098
+ for (const exportSpecifier of namedExportSpecifiers(source, identifier)) {
1099
+ const exportTarget = resolveLocalModuleSpecifier(
1100
+ normalizedSourcePath,
1101
+ exportSpecifier.specifier
1102
+ );
1103
+ if (!exportTarget) {
1104
+ continue;
1105
+ }
1106
+
1107
+ const reexportedKind = namedExportKindFromFile(
1108
+ exportTarget,
1109
+ exportSpecifier.identifier,
1110
+ new Set(visited)
1111
+ );
1112
+ if (exportKindHasValue(reexportedKind) && !exportSpecifier.typeOnly) {
1113
+ return reexportedKind;
1114
+ }
1115
+ if (exportKindHasType(reexportedKind)) {
1116
+ sawTypeExport = true;
1117
+ }
1118
+ }
1119
+
1120
+ for (const exportSpecifier of starExportSpecifiers(source)) {
1121
+ const exportTarget = resolveLocalModuleSpecifier(
1122
+ normalizedSourcePath,
1123
+ exportSpecifier.specifier
1124
+ );
1125
+ if (!exportTarget) {
1126
+ continue;
1127
+ }
1128
+
1129
+ const reexportedKind = namedExportKindFromFile(
1130
+ exportTarget,
1131
+ identifier,
1132
+ new Set(visited)
1133
+ );
1134
+ if (exportKindHasValue(reexportedKind) && !exportSpecifier.typeOnly) {
1135
+ return reexportedKind;
1136
+ }
1137
+ if (exportKindHasType(reexportedKind)) {
1138
+ sawTypeExport = true;
1139
+ }
1140
+ }
1141
+
1142
+ return sawTypeExport ? 'type' : undefined;
1143
+ };
1144
+
1145
+ const conformanceExportDiagnostics = (
1146
+ context: AdapterTargetParseContext,
1147
+ target: string,
1148
+ conformance: AdapterTargetConformanceManifest,
1149
+ testingExportTarget: string
1150
+ ): readonly AdapterTargetCatalogDiagnostic[] => {
1151
+ if (!existsSync(testingExportTarget)) {
1152
+ return [
1153
+ diagnostic(
1154
+ context,
1155
+ 'invalid-conformance',
1156
+ `Adapter target "${target}" declares conformance helpers, but the testing export source could not be read.`,
1157
+ target
1158
+ ),
1159
+ ];
1160
+ }
1161
+
1162
+ const diagnostics: AdapterTargetCatalogDiagnostic[] = [];
1163
+ for (const [field, identifier] of Object.entries(conformance) as [
1164
+ keyof AdapterTargetConformanceManifest,
1165
+ string,
1166
+ ][]) {
1167
+ const exportKind = namedExportKindFromFile(testingExportTarget, identifier);
1168
+ if (field === 'adapterType' && exportKindHasType(exportKind)) {
1169
+ continue;
1170
+ }
1171
+ if (field !== 'adapterType' && exportKindHasValue(exportKind)) {
1172
+ continue;
1173
+ }
1174
+ const expectedExport =
1175
+ field === 'adapterType' ? 'type export' : 'value export';
1176
+ diagnostics.push(
1177
+ diagnostic(
1178
+ context,
1179
+ 'invalid-conformance',
1180
+ `Adapter target "${target}" declares conformance.${field} "${identifier}", but ${context.packageName} does not provide it as a ${expectedExport} from testingImport.`,
1181
+ target
1182
+ )
1183
+ );
1184
+ }
1185
+
1186
+ return diagnostics;
1187
+ };
1188
+
1189
+ const missingExportDiagnostic = (
1190
+ context: AdapterTargetParseContext,
1191
+ field: 'supportImport' | 'testingImport',
1192
+ importSpecifier: string,
1193
+ target: string
1194
+ ): AdapterTargetCatalogDiagnostic =>
1195
+ diagnostic(
1196
+ context,
1197
+ 'invalid-import',
1198
+ `Adapter target "${target}" declares ${field} "${importSpecifier}", but ${context.packageName} does not export that subpath.`,
1199
+ target
1200
+ );
1201
+
1202
+ const missingExportTargetDiagnostic = (
1203
+ context: AdapterTargetParseContext,
1204
+ field: 'supportImport' | 'testingImport',
1205
+ importSpecifier: string,
1206
+ target: string
1207
+ ): AdapterTargetCatalogDiagnostic =>
1208
+ diagnostic(
1209
+ context,
1210
+ 'invalid-import',
1211
+ `Adapter target "${target}" declares ${field} "${importSpecifier}", but ${context.packageName} exports that subpath to a missing or non-file target.`,
1212
+ target
1213
+ );
1214
+
1215
+ const adapterTargetsRecord = (
1216
+ manifest: AdapterTargetPackageManifest
1217
+ ): Record<string, unknown> | undefined | null => {
1218
+ const trails = isRecord(manifest.trails) ? manifest.trails : undefined;
1219
+ const adapterTargets = trails?.['adapterTargets'];
1220
+ if (adapterTargets === undefined) {
1221
+ return undefined;
1222
+ }
1223
+ return isRecord(adapterTargets) ? adapterTargets : null;
1224
+ };
1225
+
1226
+ const parseCatalogTarget = (
1227
+ context: AdapterTargetParseContext,
1228
+ target: string,
1229
+ entry: unknown
1230
+ ): ParsedCatalogTarget => {
1231
+ if (!targetIdPattern.test(target) || !isRecord(entry)) {
1232
+ return {
1233
+ diagnostics: [
1234
+ diagnostic(
1235
+ context,
1236
+ 'invalid-adapter-target',
1237
+ 'Adapter target entries must use a kebab-case id and object value.',
1238
+ target
1239
+ ),
1240
+ ],
1241
+ };
1242
+ }
1243
+
1244
+ const placements = normalizePlacements(entry['placements'], context, target);
1245
+ const supportImport = normalizeOptionalImport(
1246
+ entry['supportImport'],
1247
+ 'supportImport',
1248
+ context,
1249
+ target
1250
+ );
1251
+ const testingImport = normalizeOptionalImport(
1252
+ entry['testingImport'],
1253
+ 'testingImport',
1254
+ context,
1255
+ target
1256
+ );
1257
+ const conformance = normalizeConformance(
1258
+ entry['conformance'],
1259
+ context,
1260
+ target,
1261
+ testingImport.importSpecifier !== undefined
1262
+ );
1263
+ const importDiagnostics = [
1264
+ ...placements.diagnostics,
1265
+ ...supportImport.diagnostics,
1266
+ ...testingImport.diagnostics,
1267
+ ...conformance.diagnostics,
1268
+ ];
1269
+ const supportImportSpecifier = supportImport.importSpecifier;
1270
+ const supportExportTarget = supportImportSpecifier
1271
+ ? resolveExportTargetForImport(context, supportImportSpecifier)
1272
+ : undefined;
1273
+ const testingImportSpecifier = testingImport.importSpecifier;
1274
+ const conformanceManifest = conformance.conformance;
1275
+ const testingExportTarget = testingImportSpecifier
1276
+ ? resolveExportTargetForImport(context, testingImportSpecifier)
1277
+ : undefined;
1278
+
1279
+ if (supportImportSpecifier) {
1280
+ if (!supportExportTarget) {
1281
+ importDiagnostics.push(
1282
+ missingExportDiagnostic(
1283
+ context,
1284
+ 'supportImport',
1285
+ supportImportSpecifier,
1286
+ target
1287
+ )
1288
+ );
1289
+ } else if (!pathIsFile(supportExportTarget)) {
1290
+ importDiagnostics.push(
1291
+ missingExportTargetDiagnostic(
1292
+ context,
1293
+ 'supportImport',
1294
+ supportImportSpecifier,
1295
+ target
1296
+ )
1297
+ );
1298
+ }
1299
+ }
1300
+ if (testingImportSpecifier) {
1301
+ if (!testingExportTarget) {
1302
+ importDiagnostics.push(
1303
+ missingExportDiagnostic(
1304
+ context,
1305
+ 'testingImport',
1306
+ testingImportSpecifier,
1307
+ target
1308
+ )
1309
+ );
1310
+ } else if (!pathIsFile(testingExportTarget)) {
1311
+ importDiagnostics.push(
1312
+ missingExportTargetDiagnostic(
1313
+ context,
1314
+ 'testingImport',
1315
+ testingImportSpecifier,
1316
+ target
1317
+ )
1318
+ );
1319
+ }
1320
+ }
1321
+ if (conformanceManifest && testingExportTarget) {
1322
+ importDiagnostics.push(
1323
+ ...conformanceExportDiagnostics(
1324
+ context,
1325
+ target,
1326
+ conformanceManifest,
1327
+ testingExportTarget
1328
+ )
1329
+ );
1330
+ }
1331
+ if (importDiagnostics.length > 0 || placements.placements.length === 0) {
1332
+ return { diagnostics: importDiagnostics };
1333
+ }
1334
+
1335
+ return {
1336
+ diagnostics: [],
1337
+ targetEntry: {
1338
+ key: `${context.packageName}:${target}`,
1339
+ ownerPackage: context.packageName,
1340
+ packageJsonPath: context.packageJsonPath,
1341
+ packageRoot: context.packageRoot,
1342
+ placements: placements.placements,
1343
+ ...(supportImportSpecifier
1344
+ ? {
1345
+ ...(supportExportTarget ? { supportExportTarget } : {}),
1346
+ supportImport: supportImportSpecifier,
1347
+ }
1348
+ : {}),
1349
+ target,
1350
+ ...(testingImportSpecifier
1351
+ ? {
1352
+ ...(testingExportTarget ? { testingExportTarget } : {}),
1353
+ ...(conformanceManifest
1354
+ ? { conformance: conformanceManifest }
1355
+ : {}),
1356
+ testingImport: testingImportSpecifier,
1357
+ }
1358
+ : {}),
1359
+ },
1360
+ };
1361
+ };
1362
+
1363
+ export const parseAdapterTargetsFromManifest = (
1364
+ manifest: AdapterTargetPackageManifest,
1365
+ context: AdapterTargetParseContext
1366
+ ): AdapterTargetCatalog => {
1367
+ const adapterTargets = adapterTargetsRecord(manifest);
1368
+ if (adapterTargets === undefined) {
1369
+ return { diagnostics: [], targets: [] };
1370
+ }
1371
+ if (adapterTargets === null) {
1372
+ return {
1373
+ diagnostics: [
1374
+ diagnostic(
1375
+ context,
1376
+ 'invalid-adapter-targets',
1377
+ '`trails.adapterTargets` must be an object keyed by adapter target id.'
1378
+ ),
1379
+ ],
1380
+ targets: [],
1381
+ };
1382
+ }
1383
+
1384
+ const diagnostics: AdapterTargetCatalogDiagnostic[] = [];
1385
+ const targets: AdapterTargetCatalogEntry[] = [];
1386
+
1387
+ for (const [target, entry] of Object.entries(adapterTargets).toSorted()) {
1388
+ const parsedTarget = parseCatalogTarget(context, target, entry);
1389
+ diagnostics.push(...parsedTarget.diagnostics);
1390
+ if (parsedTarget.targetEntry) {
1391
+ targets.push(parsedTarget.targetEntry);
1392
+ }
1393
+ }
1394
+
1395
+ return {
1396
+ diagnostics,
1397
+ targets,
1398
+ };
1399
+ };
1400
+
1401
+ export const deriveAdapterTargetCatalog = (
1402
+ rootDir: string
1403
+ ): AdapterTargetCatalog => {
1404
+ const normalizedRoot = normalizeRealPath(rootDir);
1405
+ const rootManifest = readJson<RootManifest>(
1406
+ join(normalizedRoot, 'package.json')
1407
+ );
1408
+ const diagnostics: AdapterTargetCatalogDiagnostic[] = [];
1409
+ const targets: AdapterTargetCatalogEntry[] = [];
1410
+
1411
+ for (const pattern of workspacePatternsFromManifest(rootManifest)) {
1412
+ for (const workspaceDir of workspaceDirsForPattern(
1413
+ normalizedRoot,
1414
+ pattern
1415
+ )) {
1416
+ const packageJsonPath = join(workspaceDir, 'package.json');
1417
+ const manifest = readJson<AdapterTargetPackageManifest>(packageJsonPath);
1418
+ if (!manifest || typeof manifest.name !== 'string') {
1419
+ continue;
1420
+ }
1421
+
1422
+ const packageRoot = normalizeRealPath(dirname(packageJsonPath));
1423
+ const normalizedExports = normalizeExportTargets(
1424
+ packageRoot,
1425
+ manifest.name,
1426
+ manifest.exports
1427
+ );
1428
+ const parsed = parseAdapterTargetsFromManifest(manifest, {
1429
+ blockedExportSpecifiers: normalizedExports.blocked,
1430
+ exportTargets: normalizedExports.targets,
1431
+ packageJsonPath: normalizeRealPath(packageJsonPath),
1432
+ packageName: manifest.name,
1433
+ packageRoot,
1434
+ });
1435
+ diagnostics.push(...parsed.diagnostics);
1436
+ targets.push(...parsed.targets);
1437
+ }
1438
+ }
1439
+
1440
+ const sortedTargets = targets.toSorted((left, right) =>
1441
+ left.key.localeCompare(right.key)
1442
+ );
1443
+ const uniqueTargets = rejectDuplicateTargetIds(sortedTargets);
1444
+
1445
+ return {
1446
+ diagnostics: [...diagnostics, ...uniqueTargets.diagnostics],
1447
+ targets: uniqueTargets.targets,
1448
+ };
1449
+ };