@ontrails/adapter-kit 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +35 -0
- package/src/catalog.ts +880 -0
- package/src/check.ts +2495 -0
- package/src/index.ts +41 -0
- package/src/overlay.ts +158 -0
- package/src/source.ts +684 -0
package/src/catalog.ts
ADDED
|
@@ -0,0 +1,880 @@
|
|
|
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 { existsSync, realpathSync, statSync } from 'node:fs';
|
|
10
|
+
import { join, resolve } from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { listWorkspacePackages } from '@ontrails/core';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
adapterSourceExportKind,
|
|
16
|
+
adapterSourceExportKindHasType,
|
|
17
|
+
adapterSourceExportKindHasValue,
|
|
18
|
+
} from './source.js';
|
|
19
|
+
|
|
20
|
+
export const adapterTargetPlacements = ['extracted', 'subpath'] as const;
|
|
21
|
+
|
|
22
|
+
export type AdapterTargetPlacementValue =
|
|
23
|
+
(typeof adapterTargetPlacements)[number];
|
|
24
|
+
|
|
25
|
+
export type AdapterTargetPlacement = AdapterTargetPlacementValue;
|
|
26
|
+
|
|
27
|
+
export interface AdapterTargetConformanceManifest {
|
|
28
|
+
readonly adapterType: string;
|
|
29
|
+
readonly casesFactory: string;
|
|
30
|
+
readonly runner: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AdapterTargetManifestEntry {
|
|
34
|
+
readonly conformance?: AdapterTargetConformanceManifest | undefined;
|
|
35
|
+
readonly placements: readonly AdapterTargetPlacement[];
|
|
36
|
+
readonly supportImport?: string | undefined;
|
|
37
|
+
readonly testingImport?: string | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AdapterTargetCatalogEntry extends AdapterTargetManifestEntry {
|
|
41
|
+
readonly key: string;
|
|
42
|
+
readonly ownerPackage: string;
|
|
43
|
+
readonly packageJsonPath: string;
|
|
44
|
+
readonly packageRoot: string;
|
|
45
|
+
readonly supportExportTarget?: string | undefined;
|
|
46
|
+
readonly target: string;
|
|
47
|
+
readonly testingExportTarget?: string | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type AdapterTargetCatalogDiagnosticCode =
|
|
51
|
+
| 'duplicate-adapter-target'
|
|
52
|
+
| 'invalid-adapter-target'
|
|
53
|
+
| 'invalid-adapter-targets'
|
|
54
|
+
| 'invalid-conformance'
|
|
55
|
+
| 'invalid-import'
|
|
56
|
+
| 'invalid-placement';
|
|
57
|
+
|
|
58
|
+
export interface AdapterTargetCatalogDiagnostic {
|
|
59
|
+
readonly code: AdapterTargetCatalogDiagnosticCode;
|
|
60
|
+
readonly message: string;
|
|
61
|
+
readonly packageJsonPath: string;
|
|
62
|
+
readonly packageName?: string | undefined;
|
|
63
|
+
readonly target?: string | undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface AdapterTargetCatalog {
|
|
67
|
+
readonly diagnostics: readonly AdapterTargetCatalogDiagnostic[];
|
|
68
|
+
readonly targets: readonly AdapterTargetCatalogEntry[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface AdapterTargetPackageManifest {
|
|
72
|
+
readonly exports?: unknown;
|
|
73
|
+
readonly name?: unknown;
|
|
74
|
+
readonly trails?: unknown;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type NamedAdapterTargetPackageManifest = AdapterTargetPackageManifest & {
|
|
78
|
+
readonly name: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export interface AdapterTargetParseContext {
|
|
82
|
+
readonly blockedExportSpecifiers: readonly string[];
|
|
83
|
+
readonly exportTargets: Readonly<Record<string, string>>;
|
|
84
|
+
readonly packageJsonPath: string;
|
|
85
|
+
readonly packageName: string;
|
|
86
|
+
readonly packageRoot: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface ParsedCatalogTarget {
|
|
90
|
+
readonly diagnostics: readonly AdapterTargetCatalogDiagnostic[];
|
|
91
|
+
readonly targetEntry?: AdapterTargetCatalogEntry | undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
95
|
+
Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
96
|
+
|
|
97
|
+
const targetIdPattern = /^[a-z][a-z0-9-]*$/u;
|
|
98
|
+
const exportIdentifierPattern = /^[A-Za-z_$][\w$]*$/u;
|
|
99
|
+
|
|
100
|
+
const normalizePath = (path: string): string => path.replaceAll('\\', '/');
|
|
101
|
+
|
|
102
|
+
const normalizeRealPath = (path: string): string => {
|
|
103
|
+
try {
|
|
104
|
+
return normalizePath(realpathSync(path));
|
|
105
|
+
} catch {
|
|
106
|
+
return normalizePath(resolve(path));
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const pathIsFile = (path: string): boolean => {
|
|
111
|
+
try {
|
|
112
|
+
return statSync(path).isFile();
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const exportConditions = new Set([
|
|
119
|
+
'bun',
|
|
120
|
+
'node',
|
|
121
|
+
'node-addons',
|
|
122
|
+
'module-sync',
|
|
123
|
+
'import',
|
|
124
|
+
'default',
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
type ResolvedExportTarget =
|
|
128
|
+
| { readonly kind: 'target'; readonly target: string }
|
|
129
|
+
| { readonly kind: 'blocked' };
|
|
130
|
+
|
|
131
|
+
const packageExportSegmentIsSafe = (segment: string): boolean => {
|
|
132
|
+
if (segment.length === 0) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
let decoded: string;
|
|
136
|
+
try {
|
|
137
|
+
decoded = decodeURIComponent(segment);
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return (
|
|
142
|
+
decoded !== '.' &&
|
|
143
|
+
decoded !== '..' &&
|
|
144
|
+
decoded.toLowerCase() !== 'node_modules' &&
|
|
145
|
+
!decoded.includes('/') &&
|
|
146
|
+
!decoded.includes('\\')
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const exportTargetIsSafe = (target: string): boolean =>
|
|
151
|
+
target.startsWith('./') &&
|
|
152
|
+
!target.includes('\\') &&
|
|
153
|
+
target.slice(2).split('/').every(packageExportSegmentIsSafe);
|
|
154
|
+
|
|
155
|
+
const resolveExportTarget = (
|
|
156
|
+
target: unknown,
|
|
157
|
+
depth = 0
|
|
158
|
+
): ResolvedExportTarget | undefined => {
|
|
159
|
+
if (typeof target === 'string') {
|
|
160
|
+
return { kind: 'target', target };
|
|
161
|
+
}
|
|
162
|
+
if (target === null) {
|
|
163
|
+
return { kind: 'blocked' };
|
|
164
|
+
}
|
|
165
|
+
if (Array.isArray(target)) {
|
|
166
|
+
if (depth > 8) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
for (const targetEntry of target) {
|
|
170
|
+
const resolvedTarget = resolveExportTarget(targetEntry, depth + 1);
|
|
171
|
+
if (
|
|
172
|
+
resolvedTarget?.kind === 'target' &&
|
|
173
|
+
exportTargetIsSafe(resolvedTarget.target)
|
|
174
|
+
) {
|
|
175
|
+
return resolvedTarget;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { kind: 'blocked' };
|
|
179
|
+
}
|
|
180
|
+
if (!isRecord(target) || depth > 8) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const [condition, conditionTarget] of Object.entries(target)) {
|
|
185
|
+
if (!exportConditions.has(condition)) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const resolvedTarget = resolveExportTarget(conditionTarget, depth + 1);
|
|
189
|
+
if (resolvedTarget) {
|
|
190
|
+
return resolvedTarget;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const exportSpecifierFromKey = (
|
|
197
|
+
packageName: string,
|
|
198
|
+
key: string
|
|
199
|
+
): string | undefined => {
|
|
200
|
+
if (key === '.') {
|
|
201
|
+
return packageName;
|
|
202
|
+
}
|
|
203
|
+
if (!key.startsWith('./')) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
return `${packageName}/${key.slice(2)}`;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const wildcardCaptureIsSafe = (capture: string): boolean =>
|
|
210
|
+
!capture.includes('\\') &&
|
|
211
|
+
capture.split('/').every(packageExportSegmentIsSafe);
|
|
212
|
+
|
|
213
|
+
const wildcardCapture = (
|
|
214
|
+
pattern: string,
|
|
215
|
+
value: string
|
|
216
|
+
): string | undefined => {
|
|
217
|
+
const star = pattern.indexOf('*');
|
|
218
|
+
if (star === -1) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const prefix = pattern.slice(0, star);
|
|
223
|
+
const suffix = pattern.slice(star + 1);
|
|
224
|
+
if (!value.startsWith(prefix) || !value.endsWith(suffix)) {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const capture = value.slice(prefix.length, value.length - suffix.length);
|
|
229
|
+
return capture.length > 0 && wildcardCaptureIsSafe(capture)
|
|
230
|
+
? capture
|
|
231
|
+
: undefined;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const applyWildcardCapture = (targetPattern: string, capture: string): string =>
|
|
235
|
+
targetPattern.replaceAll('*', capture);
|
|
236
|
+
|
|
237
|
+
type WildcardExportCandidate =
|
|
238
|
+
| {
|
|
239
|
+
readonly kind: 'target';
|
|
240
|
+
readonly pattern: string;
|
|
241
|
+
readonly target: string;
|
|
242
|
+
}
|
|
243
|
+
| { readonly kind: 'blocked'; readonly pattern: string };
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Order two wildcard export keys by Node's package-exports precedence: the
|
|
247
|
+
* longer prefix before the wildcard wins first, then the longer total key. This
|
|
248
|
+
* mirrors Node's `patternKeyCompare`, so equal-total-length patterns (for
|
|
249
|
+
* example a leading-wildcard key versus a trailing-wildcard key) resolve the
|
|
250
|
+
* way the runtime loader would.
|
|
251
|
+
*/
|
|
252
|
+
const patternKeyCompare = (left: string, right: string): number => {
|
|
253
|
+
const leftBase = left.indexOf('*') + 1;
|
|
254
|
+
const rightBase = right.indexOf('*') + 1;
|
|
255
|
+
if (leftBase !== rightBase) {
|
|
256
|
+
return rightBase - leftBase;
|
|
257
|
+
}
|
|
258
|
+
return right.length - left.length;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const resolveExportTargetForImport = (
|
|
262
|
+
context: AdapterTargetParseContext,
|
|
263
|
+
importSpecifier: string
|
|
264
|
+
): string | undefined => {
|
|
265
|
+
// Exact `null` exclusions block the subpath before any wildcard fallback.
|
|
266
|
+
if (context.blockedExportSpecifiers.includes(importSpecifier)) {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const exactTarget = context.exportTargets[importSpecifier];
|
|
271
|
+
if (exactTarget) {
|
|
272
|
+
return exactTarget;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Match the most specific wildcard key using Node's exports precedence
|
|
276
|
+
// (longer prefix before the wildcard first, then longer total key). A blocked
|
|
277
|
+
// pattern that is more specific than a broader target pattern must reject the
|
|
278
|
+
// import instead of resolving it.
|
|
279
|
+
const candidates: WildcardExportCandidate[] = [
|
|
280
|
+
...Object.entries(context.exportTargets)
|
|
281
|
+
.filter(([specifier]) => specifier.includes('*'))
|
|
282
|
+
.map(
|
|
283
|
+
([pattern, target]): WildcardExportCandidate => ({
|
|
284
|
+
kind: 'target',
|
|
285
|
+
pattern,
|
|
286
|
+
target,
|
|
287
|
+
})
|
|
288
|
+
),
|
|
289
|
+
...context.blockedExportSpecifiers
|
|
290
|
+
.filter((specifier) => specifier.includes('*'))
|
|
291
|
+
.map(
|
|
292
|
+
(pattern): WildcardExportCandidate => ({ kind: 'blocked', pattern })
|
|
293
|
+
),
|
|
294
|
+
].toSorted((left, right) => patternKeyCompare(left.pattern, right.pattern));
|
|
295
|
+
|
|
296
|
+
for (const candidate of candidates) {
|
|
297
|
+
const capture = wildcardCapture(candidate.pattern, importSpecifier);
|
|
298
|
+
if (capture === undefined) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
return candidate.kind === 'blocked'
|
|
302
|
+
? undefined
|
|
303
|
+
: applyWildcardCapture(candidate.target, capture);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return undefined;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
interface NormalizedExportTargets {
|
|
310
|
+
readonly blocked: readonly string[];
|
|
311
|
+
readonly targets: Readonly<Record<string, string>>;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const normalizeExportTargets = (
|
|
315
|
+
packageRoot: string,
|
|
316
|
+
packageName: string,
|
|
317
|
+
exportsValue: unknown
|
|
318
|
+
): NormalizedExportTargets => {
|
|
319
|
+
if (!isRecord(exportsValue)) {
|
|
320
|
+
return { blocked: [], targets: {} };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const targets: Record<string, string> = {};
|
|
324
|
+
const blocked: string[] = [];
|
|
325
|
+
for (const [key, value] of Object.entries(exportsValue)) {
|
|
326
|
+
const specifier = exportSpecifierFromKey(packageName, key);
|
|
327
|
+
if (!specifier) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const resolvedTarget = resolveExportTarget(value);
|
|
331
|
+
if (resolvedTarget?.kind === 'target') {
|
|
332
|
+
if (!exportTargetIsSafe(resolvedTarget.target)) {
|
|
333
|
+
blocked.push(specifier);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
targets[specifier] = normalizeRealPath(
|
|
337
|
+
join(packageRoot, resolvedTarget.target)
|
|
338
|
+
);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
// A declared export key that does not resolve to a runtime target (an
|
|
342
|
+
// explicit `null` exclusion, or a conditions object with no runtime
|
|
343
|
+
// condition such as a `types`-only entry) blocks the subpath. Node selects
|
|
344
|
+
// the most specific matching key and reports the subpath as not exported, so
|
|
345
|
+
// it must not fall through to a broader wildcard.
|
|
346
|
+
blocked.push(specifier);
|
|
347
|
+
}
|
|
348
|
+
return { blocked, targets };
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const diagnostic = (
|
|
352
|
+
context: AdapterTargetParseContext,
|
|
353
|
+
code: AdapterTargetCatalogDiagnosticCode,
|
|
354
|
+
message: string,
|
|
355
|
+
target?: string
|
|
356
|
+
): AdapterTargetCatalogDiagnostic => ({
|
|
357
|
+
code,
|
|
358
|
+
message,
|
|
359
|
+
packageJsonPath: context.packageJsonPath,
|
|
360
|
+
packageName: context.packageName,
|
|
361
|
+
...(target === undefined ? {} : { target }),
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const targetDiagnostic = (
|
|
365
|
+
entry: AdapterTargetCatalogEntry,
|
|
366
|
+
code: AdapterTargetCatalogDiagnosticCode,
|
|
367
|
+
message: string
|
|
368
|
+
): AdapterTargetCatalogDiagnostic => ({
|
|
369
|
+
code,
|
|
370
|
+
message,
|
|
371
|
+
packageJsonPath: entry.packageJsonPath,
|
|
372
|
+
packageName: entry.ownerPackage,
|
|
373
|
+
target: entry.target,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const rejectDuplicateTargetIds = (
|
|
377
|
+
targets: readonly AdapterTargetCatalogEntry[]
|
|
378
|
+
): {
|
|
379
|
+
readonly diagnostics: readonly AdapterTargetCatalogDiagnostic[];
|
|
380
|
+
readonly targets: readonly AdapterTargetCatalogEntry[];
|
|
381
|
+
} => {
|
|
382
|
+
const entriesByTarget = new Map<string, AdapterTargetCatalogEntry[]>();
|
|
383
|
+
for (const entry of targets) {
|
|
384
|
+
entriesByTarget.set(entry.target, [
|
|
385
|
+
...(entriesByTarget.get(entry.target) ?? []),
|
|
386
|
+
entry,
|
|
387
|
+
]);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const duplicateTargets = new Set(
|
|
391
|
+
[...entriesByTarget.entries()]
|
|
392
|
+
.filter(([, entries]) => entries.length > 1)
|
|
393
|
+
.map(([target]) => target)
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
diagnostics: [...entriesByTarget.values()]
|
|
398
|
+
.filter((entries) => entries.length > 1)
|
|
399
|
+
.flatMap((entries) =>
|
|
400
|
+
entries.map((entry) =>
|
|
401
|
+
targetDiagnostic(
|
|
402
|
+
entry,
|
|
403
|
+
'duplicate-adapter-target',
|
|
404
|
+
`Adapter target "${entry.target}" is declared by multiple owner packages; target ids must be globally unique until adapter metadata can select an owner.`
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
),
|
|
408
|
+
targets: targets.filter((entry) => !duplicateTargets.has(entry.target)),
|
|
409
|
+
};
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const isAdapterTargetPlacement = (
|
|
413
|
+
value: unknown
|
|
414
|
+
): value is AdapterTargetPlacement =>
|
|
415
|
+
typeof value === 'string' &&
|
|
416
|
+
adapterTargetPlacements.includes(value as AdapterTargetPlacement);
|
|
417
|
+
|
|
418
|
+
const normalizePlacements = (
|
|
419
|
+
value: unknown,
|
|
420
|
+
context: AdapterTargetParseContext,
|
|
421
|
+
target: string
|
|
422
|
+
): {
|
|
423
|
+
readonly diagnostics: readonly AdapterTargetCatalogDiagnostic[];
|
|
424
|
+
readonly placements: readonly AdapterTargetPlacement[];
|
|
425
|
+
} => {
|
|
426
|
+
if (!Array.isArray(value)) {
|
|
427
|
+
return {
|
|
428
|
+
diagnostics: [
|
|
429
|
+
diagnostic(
|
|
430
|
+
context,
|
|
431
|
+
'invalid-placement',
|
|
432
|
+
`Adapter target "${target}" must declare placements as an array.`,
|
|
433
|
+
target
|
|
434
|
+
),
|
|
435
|
+
],
|
|
436
|
+
placements: [],
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const diagnostics: AdapterTargetCatalogDiagnostic[] = [];
|
|
441
|
+
if (value.length === 0) {
|
|
442
|
+
diagnostics.push(
|
|
443
|
+
diagnostic(
|
|
444
|
+
context,
|
|
445
|
+
'invalid-placement',
|
|
446
|
+
`Adapter target "${target}" must declare at least one placement.`,
|
|
447
|
+
target
|
|
448
|
+
)
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const placements = new Set<AdapterTargetPlacement>();
|
|
453
|
+
for (const placement of value) {
|
|
454
|
+
if (isAdapterTargetPlacement(placement)) {
|
|
455
|
+
placements.add(placement);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
diagnostics.push(
|
|
459
|
+
diagnostic(
|
|
460
|
+
context,
|
|
461
|
+
'invalid-placement',
|
|
462
|
+
`Adapter target "${target}" has unsupported placement ${JSON.stringify(placement)}.`,
|
|
463
|
+
target
|
|
464
|
+
)
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
diagnostics,
|
|
470
|
+
placements: [...placements].toSorted(),
|
|
471
|
+
};
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const normalizeOptionalImport = (
|
|
475
|
+
value: unknown,
|
|
476
|
+
field: 'supportImport' | 'testingImport',
|
|
477
|
+
context: AdapterTargetParseContext,
|
|
478
|
+
target: string
|
|
479
|
+
): {
|
|
480
|
+
readonly diagnostics: readonly AdapterTargetCatalogDiagnostic[];
|
|
481
|
+
readonly importSpecifier?: string | undefined;
|
|
482
|
+
} => {
|
|
483
|
+
if (value === undefined) {
|
|
484
|
+
return { diagnostics: [] };
|
|
485
|
+
}
|
|
486
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
487
|
+
if (!value.startsWith(`${context.packageName}/`)) {
|
|
488
|
+
return {
|
|
489
|
+
diagnostics: [
|
|
490
|
+
diagnostic(
|
|
491
|
+
context,
|
|
492
|
+
'invalid-import',
|
|
493
|
+
`Adapter target "${target}" must declare ${field} as an owner package subpath inside ${context.packageName}.`,
|
|
494
|
+
target
|
|
495
|
+
),
|
|
496
|
+
],
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return { diagnostics: [], importSpecifier: value };
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
diagnostics: [
|
|
504
|
+
diagnostic(
|
|
505
|
+
context,
|
|
506
|
+
'invalid-import',
|
|
507
|
+
`Adapter target "${target}" must declare ${field} as a non-empty string when present.`,
|
|
508
|
+
target
|
|
509
|
+
),
|
|
510
|
+
],
|
|
511
|
+
};
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const normalizeConformance = (
|
|
515
|
+
value: unknown,
|
|
516
|
+
context: AdapterTargetParseContext,
|
|
517
|
+
target: string,
|
|
518
|
+
hasTestingImport: boolean
|
|
519
|
+
): {
|
|
520
|
+
readonly diagnostics: readonly AdapterTargetCatalogDiagnostic[];
|
|
521
|
+
readonly conformance?: AdapterTargetConformanceManifest | undefined;
|
|
522
|
+
} => {
|
|
523
|
+
if (value === undefined) {
|
|
524
|
+
return { diagnostics: [] };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (!isRecord(value)) {
|
|
528
|
+
return {
|
|
529
|
+
diagnostics: [
|
|
530
|
+
diagnostic(
|
|
531
|
+
context,
|
|
532
|
+
'invalid-conformance',
|
|
533
|
+
`Adapter target "${target}" must declare conformance as an object when present.`,
|
|
534
|
+
target
|
|
535
|
+
),
|
|
536
|
+
],
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const diagnostics: AdapterTargetCatalogDiagnostic[] = [];
|
|
541
|
+
if (!hasTestingImport) {
|
|
542
|
+
diagnostics.push(
|
|
543
|
+
diagnostic(
|
|
544
|
+
context,
|
|
545
|
+
'invalid-conformance',
|
|
546
|
+
`Adapter target "${target}" must declare testingImport before conformance helpers.`,
|
|
547
|
+
target
|
|
548
|
+
)
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const conformance: {
|
|
553
|
+
adapterType?: string | undefined;
|
|
554
|
+
casesFactory?: string | undefined;
|
|
555
|
+
runner?: string | undefined;
|
|
556
|
+
} = {};
|
|
557
|
+
for (const field of ['adapterType', 'casesFactory', 'runner'] as const) {
|
|
558
|
+
const fieldValue = value[field];
|
|
559
|
+
if (
|
|
560
|
+
typeof fieldValue === 'string' &&
|
|
561
|
+
exportIdentifierPattern.test(fieldValue)
|
|
562
|
+
) {
|
|
563
|
+
conformance[field] = fieldValue;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
diagnostics.push(
|
|
567
|
+
diagnostic(
|
|
568
|
+
context,
|
|
569
|
+
'invalid-conformance',
|
|
570
|
+
`Adapter target "${target}" must declare conformance.${field} as a valid named export.`,
|
|
571
|
+
target
|
|
572
|
+
)
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (diagnostics.length > 0) {
|
|
577
|
+
return { diagnostics };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
conformance: conformance as AdapterTargetConformanceManifest,
|
|
582
|
+
diagnostics: [],
|
|
583
|
+
};
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
const conformanceExportDiagnostics = (
|
|
587
|
+
context: AdapterTargetParseContext,
|
|
588
|
+
target: string,
|
|
589
|
+
conformance: AdapterTargetConformanceManifest,
|
|
590
|
+
testingExportTarget: string
|
|
591
|
+
): readonly AdapterTargetCatalogDiagnostic[] => {
|
|
592
|
+
if (!existsSync(testingExportTarget)) {
|
|
593
|
+
return [
|
|
594
|
+
diagnostic(
|
|
595
|
+
context,
|
|
596
|
+
'invalid-conformance',
|
|
597
|
+
`Adapter target "${target}" declares conformance helpers, but the testing export source could not be read.`,
|
|
598
|
+
target
|
|
599
|
+
),
|
|
600
|
+
];
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const diagnostics: AdapterTargetCatalogDiagnostic[] = [];
|
|
604
|
+
for (const [field, identifier] of Object.entries(conformance) as [
|
|
605
|
+
keyof AdapterTargetConformanceManifest,
|
|
606
|
+
string,
|
|
607
|
+
][]) {
|
|
608
|
+
const exportKind = adapterSourceExportKind(testingExportTarget, identifier);
|
|
609
|
+
if (field === 'adapterType' && adapterSourceExportKindHasType(exportKind)) {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (
|
|
613
|
+
field !== 'adapterType' &&
|
|
614
|
+
adapterSourceExportKindHasValue(exportKind)
|
|
615
|
+
) {
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
const expectedExport =
|
|
619
|
+
field === 'adapterType' ? 'type export' : 'value export';
|
|
620
|
+
diagnostics.push(
|
|
621
|
+
diagnostic(
|
|
622
|
+
context,
|
|
623
|
+
'invalid-conformance',
|
|
624
|
+
`Adapter target "${target}" declares conformance.${field} "${identifier}", but ${context.packageName} does not provide it as a ${expectedExport} from testingImport.`,
|
|
625
|
+
target
|
|
626
|
+
)
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return diagnostics;
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const missingExportDiagnostic = (
|
|
634
|
+
context: AdapterTargetParseContext,
|
|
635
|
+
field: 'supportImport' | 'testingImport',
|
|
636
|
+
importSpecifier: string,
|
|
637
|
+
target: string
|
|
638
|
+
): AdapterTargetCatalogDiagnostic =>
|
|
639
|
+
diagnostic(
|
|
640
|
+
context,
|
|
641
|
+
'invalid-import',
|
|
642
|
+
`Adapter target "${target}" declares ${field} "${importSpecifier}", but ${context.packageName} does not export that subpath.`,
|
|
643
|
+
target
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
const missingExportTargetDiagnostic = (
|
|
647
|
+
context: AdapterTargetParseContext,
|
|
648
|
+
field: 'supportImport' | 'testingImport',
|
|
649
|
+
importSpecifier: string,
|
|
650
|
+
target: string
|
|
651
|
+
): AdapterTargetCatalogDiagnostic =>
|
|
652
|
+
diagnostic(
|
|
653
|
+
context,
|
|
654
|
+
'invalid-import',
|
|
655
|
+
`Adapter target "${target}" declares ${field} "${importSpecifier}", but ${context.packageName} exports that subpath to a missing or non-file target.`,
|
|
656
|
+
target
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
const adapterTargetsRecord = (
|
|
660
|
+
manifest: AdapterTargetPackageManifest
|
|
661
|
+
): Record<string, unknown> | undefined | null => {
|
|
662
|
+
const trails = isRecord(manifest.trails) ? manifest.trails : undefined;
|
|
663
|
+
const adapterTargets = trails?.['adapterTargets'];
|
|
664
|
+
if (adapterTargets === undefined) {
|
|
665
|
+
return undefined;
|
|
666
|
+
}
|
|
667
|
+
return isRecord(adapterTargets) ? adapterTargets : null;
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
const parseCatalogTarget = (
|
|
671
|
+
context: AdapterTargetParseContext,
|
|
672
|
+
target: string,
|
|
673
|
+
entry: unknown
|
|
674
|
+
): ParsedCatalogTarget => {
|
|
675
|
+
if (!targetIdPattern.test(target) || !isRecord(entry)) {
|
|
676
|
+
return {
|
|
677
|
+
diagnostics: [
|
|
678
|
+
diagnostic(
|
|
679
|
+
context,
|
|
680
|
+
'invalid-adapter-target',
|
|
681
|
+
'Adapter target entries must use a kebab-case id and object value.',
|
|
682
|
+
target
|
|
683
|
+
),
|
|
684
|
+
],
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const placements = normalizePlacements(entry['placements'], context, target);
|
|
689
|
+
const supportImport = normalizeOptionalImport(
|
|
690
|
+
entry['supportImport'],
|
|
691
|
+
'supportImport',
|
|
692
|
+
context,
|
|
693
|
+
target
|
|
694
|
+
);
|
|
695
|
+
const testingImport = normalizeOptionalImport(
|
|
696
|
+
entry['testingImport'],
|
|
697
|
+
'testingImport',
|
|
698
|
+
context,
|
|
699
|
+
target
|
|
700
|
+
);
|
|
701
|
+
const conformance = normalizeConformance(
|
|
702
|
+
entry['conformance'],
|
|
703
|
+
context,
|
|
704
|
+
target,
|
|
705
|
+
testingImport.importSpecifier !== undefined
|
|
706
|
+
);
|
|
707
|
+
const importDiagnostics = [
|
|
708
|
+
...placements.diagnostics,
|
|
709
|
+
...supportImport.diagnostics,
|
|
710
|
+
...testingImport.diagnostics,
|
|
711
|
+
...conformance.diagnostics,
|
|
712
|
+
];
|
|
713
|
+
const supportImportSpecifier = supportImport.importSpecifier;
|
|
714
|
+
const supportExportTarget = supportImportSpecifier
|
|
715
|
+
? resolveExportTargetForImport(context, supportImportSpecifier)
|
|
716
|
+
: undefined;
|
|
717
|
+
const testingImportSpecifier = testingImport.importSpecifier;
|
|
718
|
+
const conformanceManifest = conformance.conformance;
|
|
719
|
+
const testingExportTarget = testingImportSpecifier
|
|
720
|
+
? resolveExportTargetForImport(context, testingImportSpecifier)
|
|
721
|
+
: undefined;
|
|
722
|
+
|
|
723
|
+
if (supportImportSpecifier) {
|
|
724
|
+
if (!supportExportTarget) {
|
|
725
|
+
importDiagnostics.push(
|
|
726
|
+
missingExportDiagnostic(
|
|
727
|
+
context,
|
|
728
|
+
'supportImport',
|
|
729
|
+
supportImportSpecifier,
|
|
730
|
+
target
|
|
731
|
+
)
|
|
732
|
+
);
|
|
733
|
+
} else if (!pathIsFile(supportExportTarget)) {
|
|
734
|
+
importDiagnostics.push(
|
|
735
|
+
missingExportTargetDiagnostic(
|
|
736
|
+
context,
|
|
737
|
+
'supportImport',
|
|
738
|
+
supportImportSpecifier,
|
|
739
|
+
target
|
|
740
|
+
)
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (testingImportSpecifier) {
|
|
745
|
+
if (!testingExportTarget) {
|
|
746
|
+
importDiagnostics.push(
|
|
747
|
+
missingExportDiagnostic(
|
|
748
|
+
context,
|
|
749
|
+
'testingImport',
|
|
750
|
+
testingImportSpecifier,
|
|
751
|
+
target
|
|
752
|
+
)
|
|
753
|
+
);
|
|
754
|
+
} else if (!pathIsFile(testingExportTarget)) {
|
|
755
|
+
importDiagnostics.push(
|
|
756
|
+
missingExportTargetDiagnostic(
|
|
757
|
+
context,
|
|
758
|
+
'testingImport',
|
|
759
|
+
testingImportSpecifier,
|
|
760
|
+
target
|
|
761
|
+
)
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (conformanceManifest && testingExportTarget) {
|
|
766
|
+
importDiagnostics.push(
|
|
767
|
+
...conformanceExportDiagnostics(
|
|
768
|
+
context,
|
|
769
|
+
target,
|
|
770
|
+
conformanceManifest,
|
|
771
|
+
testingExportTarget
|
|
772
|
+
)
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
if (importDiagnostics.length > 0 || placements.placements.length === 0) {
|
|
776
|
+
return { diagnostics: importDiagnostics };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return {
|
|
780
|
+
diagnostics: [],
|
|
781
|
+
targetEntry: {
|
|
782
|
+
key: `${context.packageName}:${target}`,
|
|
783
|
+
ownerPackage: context.packageName,
|
|
784
|
+
packageJsonPath: context.packageJsonPath,
|
|
785
|
+
packageRoot: context.packageRoot,
|
|
786
|
+
placements: placements.placements,
|
|
787
|
+
...(supportImportSpecifier
|
|
788
|
+
? {
|
|
789
|
+
...(supportExportTarget ? { supportExportTarget } : {}),
|
|
790
|
+
supportImport: supportImportSpecifier,
|
|
791
|
+
}
|
|
792
|
+
: {}),
|
|
793
|
+
target,
|
|
794
|
+
...(testingImportSpecifier
|
|
795
|
+
? {
|
|
796
|
+
...(testingExportTarget ? { testingExportTarget } : {}),
|
|
797
|
+
...(conformanceManifest
|
|
798
|
+
? { conformance: conformanceManifest }
|
|
799
|
+
: {}),
|
|
800
|
+
testingImport: testingImportSpecifier,
|
|
801
|
+
}
|
|
802
|
+
: {}),
|
|
803
|
+
},
|
|
804
|
+
};
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
export const parseAdapterTargetsFromManifest = (
|
|
808
|
+
manifest: AdapterTargetPackageManifest,
|
|
809
|
+
context: AdapterTargetParseContext
|
|
810
|
+
): AdapterTargetCatalog => {
|
|
811
|
+
const adapterTargets = adapterTargetsRecord(manifest);
|
|
812
|
+
if (adapterTargets === undefined) {
|
|
813
|
+
return { diagnostics: [], targets: [] };
|
|
814
|
+
}
|
|
815
|
+
if (adapterTargets === null) {
|
|
816
|
+
return {
|
|
817
|
+
diagnostics: [
|
|
818
|
+
diagnostic(
|
|
819
|
+
context,
|
|
820
|
+
'invalid-adapter-targets',
|
|
821
|
+
'`trails.adapterTargets` must be an object keyed by adapter target id.'
|
|
822
|
+
),
|
|
823
|
+
],
|
|
824
|
+
targets: [],
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const diagnostics: AdapterTargetCatalogDiagnostic[] = [];
|
|
829
|
+
const targets: AdapterTargetCatalogEntry[] = [];
|
|
830
|
+
|
|
831
|
+
for (const [target, entry] of Object.entries(adapterTargets).toSorted()) {
|
|
832
|
+
const parsedTarget = parseCatalogTarget(context, target, entry);
|
|
833
|
+
diagnostics.push(...parsedTarget.diagnostics);
|
|
834
|
+
if (parsedTarget.targetEntry) {
|
|
835
|
+
targets.push(parsedTarget.targetEntry);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return {
|
|
840
|
+
diagnostics,
|
|
841
|
+
targets,
|
|
842
|
+
};
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
export const deriveAdapterTargetCatalog = (
|
|
846
|
+
rootDir: string
|
|
847
|
+
): AdapterTargetCatalog => {
|
|
848
|
+
const diagnostics: AdapterTargetCatalogDiagnostic[] = [];
|
|
849
|
+
const targets: AdapterTargetCatalogEntry[] = [];
|
|
850
|
+
|
|
851
|
+
for (const workspacePackage of listWorkspacePackages<NamedAdapterTargetPackageManifest>(
|
|
852
|
+
rootDir
|
|
853
|
+
)) {
|
|
854
|
+
const { manifest, packageJsonPath, packageRoot } = workspacePackage;
|
|
855
|
+
const normalizedExports = normalizeExportTargets(
|
|
856
|
+
packageRoot,
|
|
857
|
+
manifest.name,
|
|
858
|
+
manifest.exports
|
|
859
|
+
);
|
|
860
|
+
const parsed = parseAdapterTargetsFromManifest(manifest, {
|
|
861
|
+
blockedExportSpecifiers: normalizedExports.blocked,
|
|
862
|
+
exportTargets: normalizedExports.targets,
|
|
863
|
+
packageJsonPath,
|
|
864
|
+
packageName: manifest.name,
|
|
865
|
+
packageRoot,
|
|
866
|
+
});
|
|
867
|
+
diagnostics.push(...parsed.diagnostics);
|
|
868
|
+
targets.push(...parsed.targets);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const sortedTargets = targets.toSorted((left, right) =>
|
|
872
|
+
left.key.localeCompare(right.key)
|
|
873
|
+
);
|
|
874
|
+
const uniqueTargets = rejectDuplicateTargetIds(sortedTargets);
|
|
875
|
+
|
|
876
|
+
return {
|
|
877
|
+
diagnostics: [...diagnostics, ...uniqueTargets.diagnostics],
|
|
878
|
+
targets: uniqueTargets.targets,
|
|
879
|
+
};
|
|
880
|
+
};
|