@ontrails/trails 1.0.0-beta.39 → 1.0.0-beta.41
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/CHANGELOG.md +88 -0
- package/README.md +1 -1
- package/package.json +17 -17
- package/src/app.ts +4 -4
- package/src/cli.ts +1 -1
- package/src/completions.ts +1 -1
- package/src/mcp-app.ts +2 -2
- package/src/regrade/history.ts +148 -38
- package/src/regrade/plan-artifact.ts +31 -11
- package/src/release/native-bun-registry.ts +12 -1
- package/src/release/policy.ts +4 -1
- package/src/retired-topo-command.ts +1 -1
- package/src/run-watch.ts +1 -1
- package/src/run-wayfind-outline.ts +6 -2
- package/src/trails/adapter-check.ts +8 -8
- package/src/trails/add-surface.ts +2 -2
- package/src/trails/add-trail.ts +3 -3
- package/src/trails/add-verify.ts +2 -2
- package/src/trails/compile.ts +9 -9
- package/src/trails/completions-complete.ts +10 -10
- package/src/trails/completions.ts +2 -2
- package/src/trails/create-adapter.ts +185 -424
- package/src/trails/create-scaffold.ts +12 -12
- package/src/trails/create-versions.ts +8 -8
- package/src/trails/create.ts +35 -35
- package/src/trails/deprecate.ts +2 -2
- package/src/trails/dev-clean.ts +11 -11
- package/src/trails/dev-reset.ts +11 -11
- package/src/trails/dev-stats.ts +8 -8
- package/src/trails/dev-support.ts +1 -1
- package/src/trails/doctor.ts +4 -4
- package/src/trails/draft-promote.ts +3 -3
- package/src/trails/guide.ts +9 -9
- package/src/trails/load-app.ts +3 -3
- package/src/trails/regrade.ts +56 -27
- package/src/trails/release-check.ts +8 -8
- package/src/trails/release-smoke.ts +2 -2
- package/src/trails/revise.ts +2 -2
- package/src/trails/run-example.ts +9 -9
- package/src/trails/run-examples.ts +9 -9
- package/src/trails/run.ts +26 -26
- package/src/trails/survey.ts +45 -45
- package/src/trails/topo-activation.ts +2 -2
- package/src/trails/topo-history.ts +8 -8
- package/src/trails/topo-output-schemas.ts +5 -5
- package/src/trails/topo-pin.ts +6 -6
- package/src/trails/topo-read-support.ts +4 -3
- package/src/trails/topo-reports.ts +16 -16
- package/src/trails/topo-store-support.ts +18 -9
- package/src/trails/topo-support.ts +2 -2
- package/src/trails/topo-unpin.ts +12 -12
- package/src/trails/topo.ts +4 -4
- package/src/trails/validate.ts +2 -2
- package/src/trails/version-lifecycle-support.ts +17 -16
- package/src/trails/warden-guide.ts +8 -8
- package/src/trails/warden.ts +21 -21
- package/src/trails/wayfind-outline.ts +876 -0
- package/src/trails/wayfind.ts +74 -74
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
adapterSourceExportKind,
|
|
7
|
+
adapterSourceExports,
|
|
6
8
|
adapterTargetPlacements,
|
|
7
9
|
checkAdapters,
|
|
8
10
|
deriveAdapterTargetCatalog,
|
|
@@ -22,7 +24,7 @@ import {
|
|
|
22
24
|
} from '@ontrails/core';
|
|
23
25
|
import type { WorkspaceRootManifest } from '@ontrails/core';
|
|
24
26
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
25
|
-
import { dirname, join, resolve } from 'node:path';
|
|
27
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
26
28
|
import { z } from 'zod';
|
|
27
29
|
|
|
28
30
|
import {
|
|
@@ -47,7 +49,7 @@ const packageNameMessage =
|
|
|
47
49
|
|
|
48
50
|
type CreateAdapterPlacement = AdapterTargetPlacement;
|
|
49
51
|
|
|
50
|
-
const createAdapterPlacements =
|
|
52
|
+
const createAdapterPlacements = adapterTargetPlacements;
|
|
51
53
|
|
|
52
54
|
interface CreateAdapterInput {
|
|
53
55
|
readonly dryRun: boolean;
|
|
@@ -79,18 +81,7 @@ interface AdapterOperationPlan {
|
|
|
79
81
|
interface WorkspacePackageManifest {
|
|
80
82
|
readonly exports?: unknown;
|
|
81
83
|
readonly name?: unknown;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
interface LocalNamedReexport {
|
|
85
|
-
readonly local: string;
|
|
86
|
-
readonly specifier: string;
|
|
87
|
-
readonly typeOnly: boolean;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
interface LocalNamedImport {
|
|
91
|
-
readonly imported: string;
|
|
92
|
-
readonly specifier: string;
|
|
93
|
-
readonly typeOnly: boolean;
|
|
84
|
+
readonly trails?: unknown;
|
|
94
85
|
}
|
|
95
86
|
|
|
96
87
|
const literal = (value: string): string => JSON.stringify(value);
|
|
@@ -169,6 +160,9 @@ const resolvePhysicalRootDir = (
|
|
|
169
160
|
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
|
|
170
161
|
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
171
162
|
|
|
163
|
+
const toMutableRecord = (value: unknown): Record<string, unknown> =>
|
|
164
|
+
isRecord(value) ? { ...value } : {};
|
|
165
|
+
|
|
172
166
|
const runtimeExportTarget = (value: unknown, depth = 0): string | undefined => {
|
|
173
167
|
if (typeof value === 'string') {
|
|
174
168
|
return value;
|
|
@@ -208,390 +202,13 @@ const packageRootExportTarget = (
|
|
|
208
202
|
return resolve(target.packageRoot, exportTarget);
|
|
209
203
|
};
|
|
210
204
|
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
output[cursor] = ' ';
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const skipQuoted = (quote: '"' | "'" | '`'): void => {
|
|
224
|
-
const start = index;
|
|
225
|
-
index += 1;
|
|
226
|
-
while (index < source.length) {
|
|
227
|
-
if (source[index] === '\\') {
|
|
228
|
-
index += 2;
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
if (source[index] === quote) {
|
|
232
|
-
index += 1;
|
|
233
|
-
break;
|
|
234
|
-
}
|
|
235
|
-
index += 1;
|
|
236
|
-
}
|
|
237
|
-
if (options.strings) {
|
|
238
|
-
maskRange(start, index);
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
while (index < source.length) {
|
|
243
|
-
if (source.startsWith('//', index)) {
|
|
244
|
-
const end = source.indexOf('\n', index + 2);
|
|
245
|
-
const stop = end === -1 ? source.length : end;
|
|
246
|
-
maskRange(index, stop);
|
|
247
|
-
index = stop;
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
if (source.startsWith('/*', index)) {
|
|
251
|
-
const end = source.indexOf('*/', index + 2);
|
|
252
|
-
const stop = end === -1 ? source.length : end + 2;
|
|
253
|
-
maskRange(index, stop);
|
|
254
|
-
index = stop;
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
const char = source[index];
|
|
258
|
-
if (char === '"' || char === "'" || char === '`') {
|
|
259
|
-
skipQuoted(char);
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
index += 1;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return output.join('');
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const sameFileExportListLocalForIdentifier = (
|
|
269
|
-
source: string,
|
|
270
|
-
identifier: string,
|
|
271
|
-
expected: 'type' | 'value'
|
|
272
|
-
): string | undefined => {
|
|
273
|
-
const exportCode = maskSource(source, { strings: false });
|
|
274
|
-
const stringsMaskedCode = maskSource(source, { strings: true });
|
|
275
|
-
const exportListPattern =
|
|
276
|
-
/\bexport\s+(?<typeOnly>type\s+)?\{(?<exports>[\s\S]*?)\}(?<from>\s+from\s+['"][^'"]+['"])?/gu;
|
|
277
|
-
for (const match of exportCode.matchAll(exportListPattern)) {
|
|
278
|
-
if (!stringsMaskedCode.startsWith('export', match.index ?? 0)) {
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (match.groups?.['from']) {
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const namedExports = match.groups?.['exports'] ?? '';
|
|
287
|
-
for (const item of namedExports.split(',')) {
|
|
288
|
-
const trimmedItem = item.trim();
|
|
289
|
-
const specifierTypeOnly =
|
|
290
|
-
Boolean(match.groups?.['typeOnly']) || trimmedItem.startsWith('type ');
|
|
291
|
-
const specifierText = trimmedItem.replace(/^type\s+/u, '');
|
|
292
|
-
const exported =
|
|
293
|
-
/^(?<local>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<name>[A-Za-z_$][\w$]*))?$/u.exec(
|
|
294
|
-
specifierText
|
|
295
|
-
)?.groups;
|
|
296
|
-
if (
|
|
297
|
-
!exported?.['local'] ||
|
|
298
|
-
(exported['name'] ?? exported['local']) !== identifier
|
|
299
|
-
) {
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
if (expected === 'type') {
|
|
303
|
-
return exported['local'];
|
|
304
|
-
}
|
|
305
|
-
if (!specifierTypeOnly) {
|
|
306
|
-
return exported['local'];
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return undefined;
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
const declaresTypeBinding = (source: string, identifier: string): boolean => {
|
|
315
|
-
const code = maskSource(source, { strings: true });
|
|
316
|
-
const escapedIdentifier = identifier.replaceAll(
|
|
317
|
-
/[.*+?^${}()|[\]\\]/gu,
|
|
318
|
-
'\\$&'
|
|
319
|
-
);
|
|
320
|
-
return new RegExp(
|
|
321
|
-
`(?:^|[;\\n\\r])\\s*(?:export\\s+)?(?:declare\\s+)?(?:interface|type)\\s+${escapedIdentifier}\\b`,
|
|
322
|
-
'u'
|
|
323
|
-
).test(code);
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
const declaresValueBinding = (source: string, identifier: string): boolean => {
|
|
327
|
-
const code = maskSource(source, { strings: true });
|
|
328
|
-
const escapedIdentifier = identifier.replaceAll(
|
|
329
|
-
/[.*+?^${}()|[\]\\]/gu,
|
|
330
|
-
'\\$&'
|
|
331
|
-
);
|
|
332
|
-
return new RegExp(
|
|
333
|
-
`(?:^|[;\\n\\r])\\s*(?:export\\s+)?(?:(?:async\\s+)?function|const|let|var|class|enum)\\s+${escapedIdentifier}\\b`,
|
|
334
|
-
'u'
|
|
335
|
-
).test(code);
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
const sourceExportsIdentifier = (
|
|
339
|
-
source: string,
|
|
340
|
-
identifier: string,
|
|
341
|
-
expected: 'type' | 'value'
|
|
342
|
-
): boolean => {
|
|
343
|
-
const code = maskSource(source, { strings: true });
|
|
344
|
-
const escapedIdentifier = identifier.replaceAll(
|
|
345
|
-
/[.*+?^${}()|[\]\\]/gu,
|
|
346
|
-
'\\$&'
|
|
347
|
-
);
|
|
348
|
-
const valueDeclarationPattern = new RegExp(
|
|
349
|
-
`\\bexport\\s+(?:(?:async\\s+)?function|const|let|var|class|enum)\\s+${escapedIdentifier}\\b`,
|
|
350
|
-
'u'
|
|
351
|
-
);
|
|
352
|
-
if (expected === 'value' && valueDeclarationPattern.test(code)) {
|
|
353
|
-
return true;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const typeDeclarationPattern = new RegExp(
|
|
357
|
-
`\\bexport\\s+(?:declare\\s+)?(?:interface|type)\\s+${escapedIdentifier}\\b`,
|
|
358
|
-
'u'
|
|
359
|
-
);
|
|
360
|
-
const sameFileLocal = sameFileExportListLocalForIdentifier(
|
|
361
|
-
source,
|
|
362
|
-
identifier,
|
|
363
|
-
expected
|
|
364
|
-
);
|
|
365
|
-
return (
|
|
366
|
-
(expected === 'type' &&
|
|
367
|
-
(typeDeclarationPattern.test(code) ||
|
|
368
|
-
(sameFileLocal !== undefined &&
|
|
369
|
-
declaresTypeBinding(source, sameFileLocal)))) ||
|
|
370
|
-
(expected === 'value' &&
|
|
371
|
-
sameFileLocal !== undefined &&
|
|
372
|
-
declaresValueBinding(source, sameFileLocal))
|
|
373
|
-
);
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
const localNamedImports = (
|
|
377
|
-
source: string,
|
|
378
|
-
identifier: string
|
|
379
|
-
): readonly LocalNamedImport[] => {
|
|
380
|
-
const code = maskSource(source, { strings: false });
|
|
381
|
-
const stringsMaskedCode = maskSource(source, { strings: true });
|
|
382
|
-
const imports: LocalNamedImport[] = [];
|
|
383
|
-
const pattern =
|
|
384
|
-
/\bimport\s+(?<typeOnly>type\s+)?\{(?<imports>[\s\S]*?)\}\s+from\s+['"](?<specifier>[^'"]+)['"]/gu;
|
|
385
|
-
|
|
386
|
-
for (const match of code.matchAll(pattern)) {
|
|
387
|
-
if (!stringsMaskedCode.startsWith('import', match.index ?? 0)) {
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const specifier = match.groups?.['specifier'];
|
|
392
|
-
if (!specifier?.startsWith('.')) {
|
|
393
|
-
continue;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const namedImports = match.groups?.['imports'] ?? '';
|
|
397
|
-
for (const item of namedImports.split(',')) {
|
|
398
|
-
const trimmedItem = item.trim();
|
|
399
|
-
if (!trimmedItem) {
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
const specifierTypeOnly =
|
|
404
|
-
Boolean(match.groups?.['typeOnly']) || trimmedItem.startsWith('type ');
|
|
405
|
-
const specifierText = trimmedItem.replace(/^type\s+/u, '');
|
|
406
|
-
const imported =
|
|
407
|
-
/^(?<imported>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<local>[A-Za-z_$][\w$]*))?$/u.exec(
|
|
408
|
-
specifierText
|
|
409
|
-
)?.groups;
|
|
410
|
-
if (
|
|
411
|
-
!imported?.['imported'] ||
|
|
412
|
-
(imported['local'] ?? imported['imported']) !== identifier
|
|
413
|
-
) {
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
imports.push({
|
|
418
|
-
imported: imported['imported'],
|
|
419
|
-
specifier,
|
|
420
|
-
typeOnly: specifierTypeOnly,
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
return imports;
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
const localNamedReexports = (
|
|
429
|
-
source: string,
|
|
430
|
-
identifier: string
|
|
431
|
-
): readonly LocalNamedReexport[] => {
|
|
432
|
-
const code = maskSource(source, { strings: false });
|
|
433
|
-
const stringsMaskedCode = maskSource(source, { strings: true });
|
|
434
|
-
const exports: LocalNamedReexport[] = [];
|
|
435
|
-
const pattern =
|
|
436
|
-
/\bexport\s+(?<typeOnly>type\s+)?\{(?<exports>[\s\S]*?)\}\s+from\s+['"](?<specifier>[^'"]+)['"]/gu;
|
|
437
|
-
|
|
438
|
-
for (const match of code.matchAll(pattern)) {
|
|
439
|
-
if (!stringsMaskedCode.startsWith('export', match.index ?? 0)) {
|
|
440
|
-
continue;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const specifier = match.groups?.['specifier'];
|
|
444
|
-
if (!specifier?.startsWith('.')) {
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const namedExports = match.groups?.['exports'] ?? '';
|
|
449
|
-
for (const item of namedExports.split(',')) {
|
|
450
|
-
const trimmedItem = item.trim();
|
|
451
|
-
if (!trimmedItem) {
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const specifierTypeOnly =
|
|
456
|
-
Boolean(match.groups?.['typeOnly']) || trimmedItem.startsWith('type ');
|
|
457
|
-
const specifierText = trimmedItem.replace(/^type\s+/u, '');
|
|
458
|
-
const exported =
|
|
459
|
-
/^(?<local>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<name>[A-Za-z_$][\w$]*))?$/u.exec(
|
|
460
|
-
specifierText
|
|
461
|
-
)?.groups;
|
|
462
|
-
if (
|
|
463
|
-
!exported?.['local'] ||
|
|
464
|
-
(exported['name'] ?? exported['local']) !== identifier
|
|
465
|
-
) {
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
exports.push({
|
|
470
|
-
local: exported['local'],
|
|
471
|
-
specifier,
|
|
472
|
-
typeOnly: specifierTypeOnly,
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
return exports;
|
|
478
|
-
};
|
|
479
|
-
|
|
480
|
-
const localStarReexports = (
|
|
481
|
-
source: string
|
|
482
|
-
): readonly { readonly specifier: string; readonly typeOnly: boolean }[] => {
|
|
483
|
-
const code = maskSource(source, { strings: false });
|
|
484
|
-
const stringsMaskedCode = maskSource(source, { strings: true });
|
|
485
|
-
return [
|
|
486
|
-
...code.matchAll(
|
|
487
|
-
/\bexport\s+(?<typeOnly>type\s+)?\*\s+from\s+['"](?<specifier>[^'"]+)['"]/gu
|
|
488
|
-
),
|
|
489
|
-
]
|
|
490
|
-
.filter((match) => stringsMaskedCode.startsWith('export', match.index ?? 0))
|
|
491
|
-
.map((match) => ({
|
|
492
|
-
specifier: match.groups?.['specifier'] ?? '',
|
|
493
|
-
typeOnly: Boolean(match.groups?.['typeOnly']),
|
|
494
|
-
}))
|
|
495
|
-
.filter((entry) => entry.specifier.startsWith('.'));
|
|
496
|
-
};
|
|
497
|
-
|
|
498
|
-
const resolveLocalModuleSpecifier = (
|
|
499
|
-
sourcePath: string,
|
|
500
|
-
specifier: string
|
|
501
|
-
): string | undefined => {
|
|
502
|
-
const basePath = resolve(dirname(sourcePath), specifier);
|
|
503
|
-
const candidates = [
|
|
504
|
-
basePath,
|
|
505
|
-
basePath.endsWith('.js') ? `${basePath.slice(0, -3)}.ts` : undefined,
|
|
506
|
-
basePath.endsWith('.js') ? `${basePath.slice(0, -3)}.tsx` : undefined,
|
|
507
|
-
`${basePath}.ts`,
|
|
508
|
-
`${basePath}.tsx`,
|
|
509
|
-
join(basePath, 'index.ts'),
|
|
510
|
-
join(basePath, 'index.tsx'),
|
|
511
|
-
].filter((candidate): candidate is string => candidate !== undefined);
|
|
512
|
-
|
|
513
|
-
return candidates.find((candidate) => existsSync(candidate));
|
|
514
|
-
};
|
|
515
|
-
|
|
516
|
-
const sourcePathExportsIdentifier = (
|
|
517
|
-
sourcePath: string,
|
|
518
|
-
identifier: string,
|
|
519
|
-
expected: 'type' | 'value',
|
|
520
|
-
visited = new Set<string>()
|
|
521
|
-
): boolean => {
|
|
522
|
-
const normalizedSourcePath = realpathSync(sourcePath);
|
|
523
|
-
const visitKey = `${normalizedSourcePath}:${identifier}:${expected}`;
|
|
524
|
-
if (visited.has(visitKey)) {
|
|
525
|
-
return false;
|
|
526
|
-
}
|
|
527
|
-
visited.add(visitKey);
|
|
528
|
-
|
|
529
|
-
const source = readFileSync(normalizedSourcePath, 'utf8');
|
|
530
|
-
if (sourceExportsIdentifier(source, identifier, expected)) {
|
|
531
|
-
return true;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const sameFileLocal = sameFileExportListLocalForIdentifier(
|
|
535
|
-
source,
|
|
536
|
-
identifier,
|
|
537
|
-
expected
|
|
538
|
-
);
|
|
539
|
-
if (sameFileLocal) {
|
|
540
|
-
for (const localImport of localNamedImports(source, sameFileLocal)) {
|
|
541
|
-
if (expected === 'value' && localImport.typeOnly) {
|
|
542
|
-
continue;
|
|
543
|
-
}
|
|
544
|
-
const targetPath = resolveLocalModuleSpecifier(
|
|
545
|
-
normalizedSourcePath,
|
|
546
|
-
localImport.specifier
|
|
547
|
-
);
|
|
548
|
-
if (
|
|
549
|
-
targetPath &&
|
|
550
|
-
sourcePathExportsIdentifier(
|
|
551
|
-
targetPath,
|
|
552
|
-
localImport.imported,
|
|
553
|
-
expected,
|
|
554
|
-
visited
|
|
555
|
-
)
|
|
556
|
-
) {
|
|
557
|
-
return true;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
for (const reexport of localNamedReexports(source, identifier)) {
|
|
563
|
-
if (expected === 'value' && reexport.typeOnly) {
|
|
564
|
-
continue;
|
|
565
|
-
}
|
|
566
|
-
const targetPath = resolveLocalModuleSpecifier(
|
|
567
|
-
normalizedSourcePath,
|
|
568
|
-
reexport.specifier
|
|
569
|
-
);
|
|
570
|
-
if (
|
|
571
|
-
targetPath &&
|
|
572
|
-
sourcePathExportsIdentifier(targetPath, reexport.local, expected, visited)
|
|
573
|
-
) {
|
|
574
|
-
return true;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
for (const reexport of localStarReexports(source)) {
|
|
579
|
-
if (expected === 'value' && reexport.typeOnly) {
|
|
580
|
-
continue;
|
|
581
|
-
}
|
|
582
|
-
const targetPath = resolveLocalModuleSpecifier(
|
|
583
|
-
normalizedSourcePath,
|
|
584
|
-
reexport.specifier
|
|
585
|
-
);
|
|
586
|
-
if (
|
|
587
|
-
targetPath &&
|
|
588
|
-
sourcePathExportsIdentifier(targetPath, identifier, expected, visited)
|
|
589
|
-
) {
|
|
590
|
-
return true;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
return false;
|
|
205
|
+
const relativeImportSpecifier = (fromFile: string, toFile: string): string => {
|
|
206
|
+
const withoutExtension = relative(dirname(fromFile), toFile)
|
|
207
|
+
.replaceAll('\\', '/')
|
|
208
|
+
.replace(/\.ts$/u, '.js');
|
|
209
|
+
return withoutExtension.startsWith('.')
|
|
210
|
+
? withoutExtension
|
|
211
|
+
: `./${withoutExtension}`;
|
|
595
212
|
};
|
|
596
213
|
|
|
597
214
|
const assertHttpOwnerSupport = (
|
|
@@ -605,13 +222,22 @@ const assertHttpOwnerSupport = (
|
|
|
605
222
|
}
|
|
606
223
|
|
|
607
224
|
const requiredExports = [
|
|
608
|
-
['createFetchHandler', 'value'],
|
|
609
|
-
['CreateFetchHandlerOptions', 'type'],
|
|
225
|
+
['createFetchHandler', 'value', false],
|
|
226
|
+
['CreateFetchHandlerOptions', 'type', true],
|
|
610
227
|
] as const;
|
|
611
|
-
const missing = requiredExports.filter(
|
|
612
|
-
(
|
|
613
|
-
|
|
614
|
-
|
|
228
|
+
const missing = requiredExports.filter(([identifier, expected, typeOnly]) => {
|
|
229
|
+
if (typeOnly) {
|
|
230
|
+
const exportKind = adapterSourceExportKind(rootExportTarget, identifier);
|
|
231
|
+
return (
|
|
232
|
+
exportKind !== 'type' &&
|
|
233
|
+
exportKind !== 'interface-value' &&
|
|
234
|
+
exportKind !== 'interface-value-erased' &&
|
|
235
|
+
exportKind !== 'type-alias-value' &&
|
|
236
|
+
exportKind !== 'type-alias-value-erased'
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return !adapterSourceExports(rootExportTarget, identifier, expected);
|
|
240
|
+
});
|
|
615
241
|
if (missing.length === 0) {
|
|
616
242
|
return Result.ok();
|
|
617
243
|
}
|
|
@@ -766,6 +392,21 @@ export const createApp = (
|
|
|
766
392
|
});
|
|
767
393
|
`;
|
|
768
394
|
|
|
395
|
+
const generateHttpSubpathIndex = (supportImportPath: string): string =>
|
|
396
|
+
`import type { Topo } from '@ontrails/core';
|
|
397
|
+
import { createFetchHandler } from ${literal(supportImportPath)};
|
|
398
|
+
import type { CreateFetchHandlerOptions } from ${literal(supportImportPath)};
|
|
399
|
+
|
|
400
|
+
export interface CreateAppOptions extends CreateFetchHandlerOptions {}
|
|
401
|
+
|
|
402
|
+
export const createApp = (
|
|
403
|
+
graph: Topo,
|
|
404
|
+
options: CreateAppOptions = {}
|
|
405
|
+
) => ({
|
|
406
|
+
fetch: createFetchHandler(graph, options),
|
|
407
|
+
});
|
|
408
|
+
`;
|
|
409
|
+
|
|
769
410
|
const generateConformanceTest = (
|
|
770
411
|
target: AdapterTargetCatalogEntry,
|
|
771
412
|
adapterImport: string,
|
|
@@ -881,13 +522,128 @@ const buildExtractedPlan = (
|
|
|
881
522
|
};
|
|
882
523
|
|
|
883
524
|
const buildSubpathPlan = (
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
): Result<AdapterOperationPlan, Error> =>
|
|
888
|
-
|
|
889
|
-
|
|
525
|
+
rootDir: string,
|
|
526
|
+
input: CreateAdapterInput,
|
|
527
|
+
target: AdapterTargetCatalogEntry
|
|
528
|
+
): Result<AdapterOperationPlan, Error> => {
|
|
529
|
+
if (input.packageName !== undefined) {
|
|
530
|
+
return fail(
|
|
531
|
+
'packageName is only supported for extracted adapter placement.'
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const ownerPackage = findWorkspacePackage<WorkspacePackageManifest>(
|
|
536
|
+
rootDir,
|
|
537
|
+
target.ownerPackage
|
|
538
|
+
);
|
|
539
|
+
if (!ownerPackage) {
|
|
540
|
+
return fail(
|
|
541
|
+
`Adapter target "${target.key}" owner package ${target.ownerPackage} is not a workspace package.`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const manifest = readJson<WorkspacePackageManifest>(
|
|
546
|
+
ownerPackage.packageJsonPath
|
|
547
|
+
);
|
|
548
|
+
if (!manifest || !isRecord(manifest)) {
|
|
549
|
+
return fail(
|
|
550
|
+
`Adapter target "${target.key}" owner package manifest could not be read.`
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const manifestExports = manifest['exports'];
|
|
555
|
+
if (
|
|
556
|
+
manifestExports !== undefined &&
|
|
557
|
+
typeof manifestExports !== 'string' &&
|
|
558
|
+
!isRecord(manifestExports)
|
|
559
|
+
) {
|
|
560
|
+
return fail(
|
|
561
|
+
`${target.ownerPackage} package.json exports must be an object before create.adapter can add a subpath adapter.`
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const exportKey = `./${input.name}`;
|
|
566
|
+
const packageExports =
|
|
567
|
+
typeof manifestExports === 'string'
|
|
568
|
+
? { '.': manifestExports }
|
|
569
|
+
: toMutableRecord(manifestExports);
|
|
570
|
+
if (Object.hasOwn(packageExports, exportKey)) {
|
|
571
|
+
return fail(`${target.ownerPackage} already exports "${exportKey}".`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const manifestTrails = manifest['trails'];
|
|
575
|
+
const trails = toMutableRecord(manifestTrails);
|
|
576
|
+
if (manifestTrails !== undefined && !isRecord(manifestTrails)) {
|
|
577
|
+
return fail(
|
|
578
|
+
`${target.ownerPackage} package.json trails must be an object.`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const adapters = toMutableRecord(trails['adapters']);
|
|
583
|
+
if (trails['adapters'] !== undefined && !isRecord(trails['adapters'])) {
|
|
584
|
+
return fail(
|
|
585
|
+
`${target.ownerPackage} package.json trails.adapters must be an object before create.adapter can add a subpath adapter.`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
if (Object.hasOwn(adapters, exportKey)) {
|
|
589
|
+
return fail(
|
|
590
|
+
`${target.ownerPackage} package.json trails.adapters already declares "${exportKey}".`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const sourcePath = `${ownerPackage.workspacePath}/src/${input.name}/index.ts`;
|
|
595
|
+
const sourceRoot = resolveProjectPath(
|
|
596
|
+
rootDir,
|
|
597
|
+
`${ownerPackage.workspacePath}/src/${input.name}`
|
|
890
598
|
);
|
|
599
|
+
if (sourceRoot.isErr()) {
|
|
600
|
+
return sourceRoot;
|
|
601
|
+
}
|
|
602
|
+
if (existsSync(sourceRoot.value)) {
|
|
603
|
+
return fail(
|
|
604
|
+
`Subpath adapter source already exists at ${ownerPackage.workspacePath}/src/${input.name}.`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const supportTarget = packageRootExportTarget(target);
|
|
609
|
+
if (!supportTarget) {
|
|
610
|
+
return fail(
|
|
611
|
+
`Adapter target "${target.key}" cannot use the HTTP subpath template because ${target.ownerPackage} does not expose a readable package root export.`
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
packageExports[exportKey] = `./src/${input.name}/index.ts`;
|
|
616
|
+
adapters[exportKey] = { target: target.target };
|
|
617
|
+
trails['adapters'] = adapters;
|
|
618
|
+
|
|
619
|
+
const adapterImport = `${target.ownerPackage}/${input.name}`;
|
|
620
|
+
const sourceFile = resolve(rootDir, sourcePath);
|
|
621
|
+
const packageJson = formatJson({
|
|
622
|
+
...manifest,
|
|
623
|
+
exports: packageExports,
|
|
624
|
+
trails,
|
|
625
|
+
});
|
|
626
|
+
const operations = [
|
|
627
|
+
writeOperation(`${ownerPackage.workspacePath}/package.json`, packageJson),
|
|
628
|
+
writeOperation(
|
|
629
|
+
sourcePath,
|
|
630
|
+
generateHttpSubpathIndex(
|
|
631
|
+
relativeImportSpecifier(sourceFile, supportTarget)
|
|
632
|
+
)
|
|
633
|
+
),
|
|
634
|
+
writeOperation(
|
|
635
|
+
`${ownerPackage.workspacePath}/src/${input.name}/__tests__/conformance.test.ts`,
|
|
636
|
+
generateConformanceTest(target, adapterImport, '../index.js')
|
|
637
|
+
),
|
|
638
|
+
];
|
|
639
|
+
|
|
640
|
+
return Result.ok({
|
|
641
|
+
adapterImport,
|
|
642
|
+
operations,
|
|
643
|
+
packageName: adapterImport,
|
|
644
|
+
targetKey: target.key,
|
|
645
|
+
});
|
|
646
|
+
};
|
|
891
647
|
|
|
892
648
|
const buildOperationPlan = (
|
|
893
649
|
rootDir: string,
|
|
@@ -909,7 +665,24 @@ const runPlannedOperations = async (
|
|
|
909
665
|
|
|
910
666
|
export const createAdapterTrail = trail('create.adapter', {
|
|
911
667
|
args: ['name'],
|
|
912
|
-
|
|
668
|
+
description: 'Scaffold an adapter package from adapter target catalog facts',
|
|
669
|
+
fields: {
|
|
670
|
+
placement: {
|
|
671
|
+
options: [
|
|
672
|
+
{
|
|
673
|
+
hint: 'Standalone package under adapters/',
|
|
674
|
+
label: 'Extracted',
|
|
675
|
+
value: 'extracted',
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
hint: 'Owner package subpath export',
|
|
679
|
+
label: 'Subpath',
|
|
680
|
+
value: 'subpath',
|
|
681
|
+
},
|
|
682
|
+
],
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
implementation: async (input: CreateAdapterInput, ctx) => {
|
|
913
686
|
const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
|
|
914
687
|
if (rootDirResult.isErr()) {
|
|
915
688
|
return rootDirResult;
|
|
@@ -951,23 +724,11 @@ export const createAdapterTrail = trail('create.adapter', {
|
|
|
951
724
|
diagnostics: [...report.diagnostics],
|
|
952
725
|
dryRun: input.dryRun,
|
|
953
726
|
packageName: plan.value.packageName,
|
|
954
|
-
placement:
|
|
727
|
+
placement: input.placement,
|
|
955
728
|
plannedOperations: [...plannedOperations.value],
|
|
956
729
|
targetKey: plan.value.targetKey,
|
|
957
730
|
} satisfies CreateAdapterResult);
|
|
958
731
|
},
|
|
959
|
-
description: 'Scaffold an adapter package from adapter target catalog facts',
|
|
960
|
-
fields: {
|
|
961
|
-
placement: {
|
|
962
|
-
options: [
|
|
963
|
-
{
|
|
964
|
-
hint: 'Standalone package under adapters/',
|
|
965
|
-
label: 'Extracted',
|
|
966
|
-
value: 'extracted',
|
|
967
|
-
},
|
|
968
|
-
],
|
|
969
|
-
},
|
|
970
|
-
},
|
|
971
732
|
input: z.object({
|
|
972
733
|
dryRun: z
|
|
973
734
|
.boolean()
|