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