@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/package.json +23 -0
- package/src/catalog.ts +1449 -0
- package/src/check.ts +2064 -0
- package/src/index.ts +25 -0
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
|
+
};
|