@ontrails/trails 1.0.0-beta.19 → 1.0.0-beta.21
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 +83 -0
- package/README.md +2 -0
- package/package.json +19 -13
- package/src/app.ts +43 -1
- package/src/cli.ts +10 -1
- package/src/load-app-mirror.ts +42 -0
- package/src/mcp-app.ts +30 -0
- package/src/mcp-options.ts +77 -0
- package/src/mcp.ts +8 -0
- package/src/release/bindings.ts +39 -0
- package/src/release/check.ts +818 -0
- package/src/release/config.ts +63 -0
- package/src/release/contract-facts.ts +425 -0
- package/src/release/index.ts +85 -0
- package/src/release/native-bun-publish.ts +651 -0
- package/src/release/native-bun-registry.ts +350 -0
- package/src/release/packed-artifacts-smoke.ts +236 -0
- package/src/release/smoke.ts +46 -0
- package/src/release/wayfinder-dogfood-smoke.ts +226 -0
- package/src/run-release-check.ts +74 -0
- package/src/scaffold-version-sync.ts +183 -0
- package/src/scaffold-versions.generated.ts +1 -1
- package/src/trails/compile.ts +13 -9
- package/src/trails/create-versions.ts +62 -0
- package/src/trails/guide.ts +10 -6
- package/src/trails/load-app.ts +440 -49
- package/src/trails/release-check.ts +104 -0
- package/src/trails/release-smoke.ts +48 -0
- package/src/trails/run-example.ts +17 -13
- package/src/trails/run-examples.ts +16 -12
- package/src/trails/run.ts +22 -18
- package/src/trails/topo-history.ts +12 -8
package/src/trails/load-app.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
readFileSync,
|
|
5
5
|
realpathSync,
|
|
6
6
|
statSync,
|
|
7
|
+
symlinkSync,
|
|
7
8
|
} from 'node:fs';
|
|
8
9
|
import {
|
|
9
10
|
dirname,
|
|
@@ -17,7 +18,6 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
17
18
|
|
|
18
19
|
import {
|
|
19
20
|
deriveSafePath,
|
|
20
|
-
InternalError,
|
|
21
21
|
isTrailsError,
|
|
22
22
|
PermissionError,
|
|
23
23
|
Result,
|
|
@@ -28,6 +28,7 @@ import { findAppModule } from '@ontrails/cli';
|
|
|
28
28
|
|
|
29
29
|
import {
|
|
30
30
|
createLoadAppMirrorRootPath,
|
|
31
|
+
ensureLoadAppMirrorDirectory,
|
|
31
32
|
LOAD_APP_MIRROR_ENTRY_PREFIX,
|
|
32
33
|
LOAD_APP_MIRROR_PARENT_DIRNAME,
|
|
33
34
|
removeLoadAppMirrorRootQuietly,
|
|
@@ -161,12 +162,19 @@ const trustBoundaryError = (reason: string): PermissionError =>
|
|
|
161
162
|
`Refusing to load an app module outside the workspace trust boundary (${reason}). Use a workspace-relative module path, or pass trustedModulePath: true from trusted code.`
|
|
162
163
|
);
|
|
163
164
|
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
const asError = (error: unknown): Error =>
|
|
166
|
+
error instanceof Error ? error : new Error(String(error));
|
|
167
|
+
|
|
168
|
+
const toLoadAppError = (error: unknown): Error => {
|
|
169
|
+
if (isTrailsError(error)) {
|
|
170
|
+
return error;
|
|
171
|
+
}
|
|
172
|
+
const cause = asError(error);
|
|
173
|
+
return new ValidationError(`Failed to load app module: ${cause.message}`, {
|
|
174
|
+
cause,
|
|
175
|
+
context: { detail: cause.message },
|
|
176
|
+
});
|
|
177
|
+
};
|
|
170
178
|
|
|
171
179
|
const isPathInside = (root: string, target: string): boolean => {
|
|
172
180
|
const candidate = relative(root, target);
|
|
@@ -260,19 +268,31 @@ const isLocalFilesystemImport = (importPath: string): boolean =>
|
|
|
260
268
|
importPath.startsWith('/') ||
|
|
261
269
|
importPath.startsWith('file:');
|
|
262
270
|
|
|
263
|
-
|
|
271
|
+
interface PackageJson {
|
|
272
|
+
readonly exports?: unknown;
|
|
273
|
+
readonly imports?: unknown;
|
|
274
|
+
readonly main?: unknown;
|
|
275
|
+
readonly module?: unknown;
|
|
276
|
+
readonly name?: unknown;
|
|
277
|
+
readonly type?: unknown;
|
|
278
|
+
readonly workspaces?: unknown;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const readPackageJson = (packagePath: string): PackageJson | undefined => {
|
|
264
282
|
try {
|
|
265
|
-
|
|
266
|
-
| { readonly name?: unknown }
|
|
267
|
-
| undefined;
|
|
268
|
-
return typeof parsed?.name === 'string' && parsed.name.length > 0
|
|
269
|
-
? parsed.name
|
|
270
|
-
: undefined;
|
|
283
|
+
return JSON.parse(readFileSync(packagePath, 'utf8')) as PackageJson;
|
|
271
284
|
} catch {
|
|
272
285
|
return undefined;
|
|
273
286
|
}
|
|
274
287
|
};
|
|
275
288
|
|
|
289
|
+
const readPackageName = (packagePath: string): string | undefined => {
|
|
290
|
+
const parsed = readPackageJson(packagePath);
|
|
291
|
+
return typeof parsed?.name === 'string' && parsed.name.length > 0
|
|
292
|
+
? parsed.name
|
|
293
|
+
: undefined;
|
|
294
|
+
};
|
|
295
|
+
|
|
276
296
|
const findNearestPackageName = (directoryPath: string): string | undefined => {
|
|
277
297
|
let current = directoryPath;
|
|
278
298
|
while (true) {
|
|
@@ -304,13 +324,6 @@ const isPackageLocalImport = (
|
|
|
304
324
|
);
|
|
305
325
|
};
|
|
306
326
|
|
|
307
|
-
const shouldMirrorImportSpecifier = (
|
|
308
|
-
importerPath: string,
|
|
309
|
-
importPath: string
|
|
310
|
-
): boolean =>
|
|
311
|
-
isLocalFilesystemImport(importPath) ||
|
|
312
|
-
isPackageLocalImport(importerPath, importPath);
|
|
313
|
-
|
|
314
327
|
const isScannableModule = (modulePath: string): boolean =>
|
|
315
328
|
SCANNABLE_EXTENSIONS.has(extname(modulePath));
|
|
316
329
|
|
|
@@ -328,10 +341,313 @@ const resolveImportedModulePath = (
|
|
|
328
341
|
);
|
|
329
342
|
};
|
|
330
343
|
|
|
331
|
-
const
|
|
344
|
+
const readDirectoryEntries = (directoryPath: string): readonly string[] => {
|
|
345
|
+
try {
|
|
346
|
+
return readdirSync(directoryPath);
|
|
347
|
+
} catch {
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const safeStat = (
|
|
353
|
+
entryPath: string
|
|
354
|
+
): ReturnType<typeof statSync> | undefined => {
|
|
355
|
+
try {
|
|
356
|
+
return statSync(entryPath);
|
|
357
|
+
} catch {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const readWorkspacePatterns = (cwd: string): readonly string[] => {
|
|
363
|
+
const rootPackage = readPackageJson(join(cwd, 'package.json'));
|
|
364
|
+
const workspaces = rootPackage?.workspaces;
|
|
365
|
+
if (Array.isArray(workspaces)) {
|
|
366
|
+
return workspaces.filter(
|
|
367
|
+
(pattern): pattern is string => typeof pattern === 'string'
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
if (
|
|
371
|
+
typeof workspaces === 'object' &&
|
|
372
|
+
workspaces !== null &&
|
|
373
|
+
'packages' in workspaces &&
|
|
374
|
+
Array.isArray(workspaces.packages)
|
|
375
|
+
) {
|
|
376
|
+
return workspaces.packages.filter(
|
|
377
|
+
(pattern): pattern is string => typeof pattern === 'string'
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
return [];
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
interface WorkspacePackage {
|
|
384
|
+
readonly name: string;
|
|
385
|
+
readonly packageJson: PackageJson;
|
|
386
|
+
readonly packageRoot: string;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const listWorkspacePackageRoots = (cwd: string): readonly string[] => {
|
|
390
|
+
const roots: string[] = [];
|
|
391
|
+
for (const pattern of readWorkspacePatterns(cwd)) {
|
|
392
|
+
if (pattern.endsWith('/*')) {
|
|
393
|
+
const baseDir = resolve(cwd, pattern.slice(0, -2));
|
|
394
|
+
for (const entry of readDirectoryEntries(baseDir)) {
|
|
395
|
+
const entryPath = join(baseDir, entry);
|
|
396
|
+
if (safeStat(entryPath)?.isDirectory()) {
|
|
397
|
+
roots.push(entryPath);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (!pattern.includes('*')) {
|
|
404
|
+
roots.push(resolve(cwd, pattern));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return roots;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const readWorkspacePackages = (cwd: string): readonly WorkspacePackage[] => {
|
|
411
|
+
const packages: WorkspacePackage[] = [];
|
|
412
|
+
for (const packageRoot of listWorkspacePackageRoots(cwd)) {
|
|
413
|
+
const packageJson = readPackageJson(join(packageRoot, 'package.json'));
|
|
414
|
+
if (
|
|
415
|
+
packageJson !== undefined &&
|
|
416
|
+
typeof packageJson.name === 'string' &&
|
|
417
|
+
packageJson.name.length > 0
|
|
418
|
+
) {
|
|
419
|
+
packages.push({
|
|
420
|
+
name: packageJson.name,
|
|
421
|
+
packageJson,
|
|
422
|
+
packageRoot,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return packages;
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const parsePackageSpecifier = (
|
|
430
|
+
importPath: string
|
|
431
|
+
): { packageName: string; subpath: string } | null => {
|
|
432
|
+
if (URL_SCHEME.test(importPath)) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
if (importPath.startsWith('.') || importPath.startsWith('#')) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
const segments = importPath.split('/');
|
|
439
|
+
const [firstSegment, secondSegment] = segments;
|
|
440
|
+
if (firstSegment?.startsWith('@')) {
|
|
441
|
+
const scope = firstSegment;
|
|
442
|
+
const name = secondSegment;
|
|
443
|
+
if (name === undefined) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
const packageName = `${scope}/${name}`;
|
|
447
|
+
const rest = segments.slice(2).join('/');
|
|
448
|
+
return { packageName, subpath: rest.length > 0 ? `./${rest}` : '.' };
|
|
449
|
+
}
|
|
450
|
+
const [name] = segments;
|
|
451
|
+
if (name === undefined || name.length === 0) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
const rest = segments.slice(1).join('/');
|
|
455
|
+
return { packageName: name, subpath: rest.length > 0 ? `./${rest}` : '.' };
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const resolveConditionalExportTarget = (
|
|
459
|
+
target: unknown
|
|
460
|
+
): string | undefined => {
|
|
461
|
+
if (typeof target === 'string') {
|
|
462
|
+
return target;
|
|
463
|
+
}
|
|
464
|
+
if (typeof target !== 'object' || target === null || Array.isArray(target)) {
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
const record = target as Record<string, unknown>;
|
|
468
|
+
return (
|
|
469
|
+
resolveConditionalExportTarget(record['import']) ??
|
|
470
|
+
resolveConditionalExportTarget(record['default']) ??
|
|
471
|
+
resolveConditionalExportTarget(record['bun']) ??
|
|
472
|
+
resolveConditionalExportTarget(record['node'])
|
|
473
|
+
);
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const resolveExportTarget = (
|
|
477
|
+
packageJson: PackageJson,
|
|
478
|
+
subpath: string
|
|
479
|
+
): string | undefined => {
|
|
480
|
+
const { exports } = packageJson;
|
|
481
|
+
if (exports === undefined) {
|
|
482
|
+
if (subpath === '.') {
|
|
483
|
+
if (typeof packageJson.module === 'string') {
|
|
484
|
+
return packageJson.module;
|
|
485
|
+
}
|
|
486
|
+
if (typeof packageJson.main === 'string') {
|
|
487
|
+
return packageJson.main;
|
|
488
|
+
}
|
|
489
|
+
return './src/index.ts';
|
|
490
|
+
}
|
|
491
|
+
return subpath;
|
|
492
|
+
}
|
|
493
|
+
if (typeof exports === 'string' || Array.isArray(exports)) {
|
|
494
|
+
return subpath === '.'
|
|
495
|
+
? resolveConditionalExportTarget(exports)
|
|
496
|
+
: undefined;
|
|
497
|
+
}
|
|
498
|
+
if (typeof exports !== 'object' || exports === null) {
|
|
499
|
+
return undefined;
|
|
500
|
+
}
|
|
501
|
+
return resolveConditionalExportTarget(
|
|
502
|
+
(exports as Record<string, unknown>)[subpath]
|
|
503
|
+
);
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
interface WorkspacePackageResolution {
|
|
507
|
+
readonly modulePath: string;
|
|
508
|
+
readonly packageName: string;
|
|
509
|
+
readonly packageRoot: string;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const resolveWorkspacePackageImport = (
|
|
513
|
+
importPath: string,
|
|
514
|
+
cwd: string
|
|
515
|
+
): WorkspacePackageResolution | null => {
|
|
516
|
+
const parsed = parsePackageSpecifier(importPath);
|
|
517
|
+
if (parsed === null) {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
const workspacePackage = readWorkspacePackages(cwd).find(
|
|
521
|
+
(candidate) => candidate.name === parsed.packageName
|
|
522
|
+
);
|
|
523
|
+
if (workspacePackage === undefined) {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
const target = resolveExportTarget(
|
|
527
|
+
workspacePackage.packageJson,
|
|
528
|
+
parsed.subpath
|
|
529
|
+
);
|
|
530
|
+
if (target === undefined || !target.startsWith('.')) {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
const targetPath = deriveSafePath(workspacePackage.packageRoot, target);
|
|
534
|
+
if (targetPath.isErr()) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
modulePath: resolveFilesystemModulePath(
|
|
539
|
+
ensureRealPathInsideCwd(targetPath.value, cwd),
|
|
540
|
+
workspacePackage.packageRoot
|
|
541
|
+
),
|
|
542
|
+
packageName: workspacePackage.name,
|
|
543
|
+
packageRoot: workspacePackage.packageRoot,
|
|
544
|
+
};
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const findPackageRootForName = (
|
|
548
|
+
directoryPath: string,
|
|
549
|
+
packageName: string
|
|
550
|
+
): string | null => {
|
|
551
|
+
let current = directoryPath;
|
|
552
|
+
while (true) {
|
|
553
|
+
const packagePath = join(current, 'package.json');
|
|
554
|
+
if (readPackageName(packagePath) === packageName) {
|
|
555
|
+
return current;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const parent = dirname(current);
|
|
559
|
+
if (parent === current) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
current = parent;
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
interface ExternalPackageResolution {
|
|
567
|
+
readonly packageName: string;
|
|
568
|
+
readonly packageRoot: string;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const resolveExternalPackageImport = (
|
|
572
|
+
importerPath: string,
|
|
573
|
+
importPath: string
|
|
574
|
+
): ExternalPackageResolution | null => {
|
|
575
|
+
const parsed = parsePackageSpecifier(importPath);
|
|
576
|
+
if (parsed === null) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
let resolved: string;
|
|
580
|
+
try {
|
|
581
|
+
resolved = import.meta.resolve(
|
|
582
|
+
importPath,
|
|
583
|
+
pathToFileURL(importerPath).href
|
|
584
|
+
);
|
|
585
|
+
} catch {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
if (!resolved.startsWith('file:')) {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
const packageRoot = findPackageRootForName(
|
|
592
|
+
dirname(fileURLToPath(resolved)),
|
|
593
|
+
parsed.packageName
|
|
594
|
+
);
|
|
595
|
+
return packageRoot === null
|
|
596
|
+
? null
|
|
597
|
+
: { packageName: parsed.packageName, packageRoot };
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
type MirrorImportResolution =
|
|
601
|
+
| {
|
|
602
|
+
readonly kind: 'module';
|
|
603
|
+
readonly modulePath: string;
|
|
604
|
+
}
|
|
605
|
+
| {
|
|
606
|
+
readonly kind: 'external-package';
|
|
607
|
+
readonly packageName: string;
|
|
608
|
+
readonly packageRoot: string;
|
|
609
|
+
}
|
|
610
|
+
| {
|
|
611
|
+
readonly kind: 'workspace-package';
|
|
612
|
+
readonly modulePath: string;
|
|
613
|
+
readonly packageName: string;
|
|
614
|
+
readonly packageRoot: string;
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const resolveMirrorImport = (
|
|
618
|
+
importerPath: string,
|
|
619
|
+
importPath: string,
|
|
620
|
+
cwd: string
|
|
621
|
+
): MirrorImportResolution | null => {
|
|
622
|
+
if (
|
|
623
|
+
isLocalFilesystemImport(importPath) ||
|
|
624
|
+
isPackageLocalImport(importerPath, importPath)
|
|
625
|
+
) {
|
|
626
|
+
return {
|
|
627
|
+
kind: 'module',
|
|
628
|
+
modulePath: resolveImportedModulePath(importerPath, importPath),
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const workspacePackage = resolveWorkspacePackageImport(importPath, cwd);
|
|
633
|
+
if (workspacePackage !== null) {
|
|
634
|
+
return { kind: 'workspace-package', ...workspacePackage };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const externalPackage = resolveExternalPackageImport(
|
|
638
|
+
importerPath,
|
|
639
|
+
importPath
|
|
640
|
+
);
|
|
641
|
+
return externalPackage === null
|
|
642
|
+
? null
|
|
643
|
+
: { kind: 'external-package', ...externalPackage };
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const collectImportedModuleResolutions = (
|
|
332
647
|
modulePath: string,
|
|
333
|
-
source: string
|
|
334
|
-
|
|
648
|
+
source: string,
|
|
649
|
+
cwd: string
|
|
650
|
+
): readonly MirrorImportResolution[] => {
|
|
335
651
|
const extension = extname(modulePath);
|
|
336
652
|
const loader = LOADER_BY_EXTENSION[extension];
|
|
337
653
|
if (loader === undefined) {
|
|
@@ -341,8 +657,10 @@ const collectImportedModulePaths = (
|
|
|
341
657
|
return getImportScanner(loader)
|
|
342
658
|
.scanImports(source)
|
|
343
659
|
.map((entry) => entry.path)
|
|
344
|
-
.
|
|
345
|
-
.
|
|
660
|
+
.map((importPath) => resolveMirrorImport(modulePath, importPath, cwd))
|
|
661
|
+
.filter(
|
|
662
|
+
(resolution): resolution is MirrorImportResolution => resolution !== null
|
|
663
|
+
);
|
|
346
664
|
};
|
|
347
665
|
|
|
348
666
|
const copyFileToMirror = async (
|
|
@@ -398,24 +716,6 @@ const MIRROR_SKIP_DIRECTORIES = new Set([
|
|
|
398
716
|
* root at runtime without pulling in package installs or nested mirror
|
|
399
717
|
* artifacts.
|
|
400
718
|
*/
|
|
401
|
-
const readDirectoryEntries = (directoryPath: string): readonly string[] => {
|
|
402
|
-
try {
|
|
403
|
-
return readdirSync(directoryPath);
|
|
404
|
-
} catch {
|
|
405
|
-
return [];
|
|
406
|
-
}
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
const safeStat = (
|
|
410
|
-
entryPath: string
|
|
411
|
-
): ReturnType<typeof statSync> | undefined => {
|
|
412
|
-
try {
|
|
413
|
-
return statSync(entryPath);
|
|
414
|
-
} catch {
|
|
415
|
-
return undefined;
|
|
416
|
-
}
|
|
417
|
-
};
|
|
418
|
-
|
|
419
719
|
/**
|
|
420
720
|
* Age threshold (ms) above which a mirror entry in `.trails-tmp/` is
|
|
421
721
|
* considered stale and safe to remove opportunistically.
|
|
@@ -480,9 +780,11 @@ const freshMirrorRootPath = (cwd: string): string => {
|
|
|
480
780
|
};
|
|
481
781
|
|
|
482
782
|
interface MirrorWalkContext {
|
|
783
|
+
readonly cwd: string;
|
|
483
784
|
readonly mirrorRoot: string;
|
|
484
785
|
readonly copied: Set<string>;
|
|
485
786
|
readonly visitedDirectories: Set<string>;
|
|
787
|
+
readonly linkedPackageNames: Set<string>;
|
|
486
788
|
}
|
|
487
789
|
|
|
488
790
|
type DirectoryEntryKind = 'directory' | 'file' | 'skip';
|
|
@@ -542,6 +844,67 @@ const copyNearestPackageJsonToMirror = async (
|
|
|
542
844
|
}
|
|
543
845
|
};
|
|
544
846
|
|
|
847
|
+
const packageLinkSegments = (packageName: string): readonly string[] =>
|
|
848
|
+
packageName.split('/').filter((segment) => segment.length > 0);
|
|
849
|
+
|
|
850
|
+
const createPackageMirrorLink = (
|
|
851
|
+
packageName: string,
|
|
852
|
+
targetRoot: string,
|
|
853
|
+
context: MirrorWalkContext
|
|
854
|
+
): void => {
|
|
855
|
+
if (context.linkedPackageNames.has(packageName)) {
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const mirrorWorkspaceRoot = resolveLoadAppMirrorFilePath(
|
|
859
|
+
context.cwd,
|
|
860
|
+
context.mirrorRoot
|
|
861
|
+
);
|
|
862
|
+
if (mirrorWorkspaceRoot.isErr()) {
|
|
863
|
+
throw mirrorWorkspaceRoot.error;
|
|
864
|
+
}
|
|
865
|
+
const linkPath = join(
|
|
866
|
+
mirrorWorkspaceRoot.value,
|
|
867
|
+
'node_modules',
|
|
868
|
+
...packageLinkSegments(packageName)
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
const ensured = ensureLoadAppMirrorDirectory(
|
|
872
|
+
dirname(linkPath),
|
|
873
|
+
context.mirrorRoot
|
|
874
|
+
);
|
|
875
|
+
if (ensured.isErr()) {
|
|
876
|
+
throw ensured.error;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
try {
|
|
880
|
+
symlinkSync(targetRoot, linkPath, 'dir');
|
|
881
|
+
} catch (error) {
|
|
882
|
+
if (
|
|
883
|
+
!(error instanceof Error) ||
|
|
884
|
+
!('code' in error) ||
|
|
885
|
+
error.code !== 'EEXIST'
|
|
886
|
+
) {
|
|
887
|
+
throw error;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
context.linkedPackageNames.add(packageName);
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
const createWorkspacePackageMirrorLink = (
|
|
894
|
+
packageName: string,
|
|
895
|
+
packageRoot: string,
|
|
896
|
+
context: MirrorWalkContext
|
|
897
|
+
): void => {
|
|
898
|
+
const mirrorPackageRoot = resolveLoadAppMirrorFilePath(
|
|
899
|
+
packageRoot,
|
|
900
|
+
context.mirrorRoot
|
|
901
|
+
);
|
|
902
|
+
if (mirrorPackageRoot.isErr()) {
|
|
903
|
+
throw mirrorPackageRoot.error;
|
|
904
|
+
}
|
|
905
|
+
createPackageMirrorLink(packageName, mirrorPackageRoot.value, context);
|
|
906
|
+
};
|
|
907
|
+
|
|
545
908
|
const mirrorImportedModule = async (
|
|
546
909
|
modulePath: string,
|
|
547
910
|
context: MirrorWalkContext
|
|
@@ -557,24 +920,47 @@ const mirrorImportedModule = async (
|
|
|
557
920
|
|
|
558
921
|
const scanAndVisitLocalImports = async (
|
|
559
922
|
modulePath: string,
|
|
923
|
+
context: MirrorWalkContext,
|
|
560
924
|
visit: (path: string) => Promise<void>
|
|
561
925
|
): Promise<void> => {
|
|
562
926
|
if (!isScannableModule(modulePath)) {
|
|
563
927
|
return;
|
|
564
928
|
}
|
|
565
929
|
const source = await Bun.file(modulePath).text();
|
|
566
|
-
for (const
|
|
567
|
-
|
|
930
|
+
for (const imported of collectImportedModuleResolutions(
|
|
931
|
+
modulePath,
|
|
932
|
+
source,
|
|
933
|
+
context.cwd
|
|
934
|
+
)) {
|
|
935
|
+
if (imported.kind === 'external-package') {
|
|
936
|
+
createPackageMirrorLink(
|
|
937
|
+
imported.packageName,
|
|
938
|
+
imported.packageRoot,
|
|
939
|
+
context
|
|
940
|
+
);
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
if (imported.kind === 'workspace-package') {
|
|
944
|
+
createWorkspacePackageMirrorLink(
|
|
945
|
+
imported.packageName,
|
|
946
|
+
imported.packageRoot,
|
|
947
|
+
context
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
await visit(imported.modulePath);
|
|
568
951
|
}
|
|
569
952
|
};
|
|
570
953
|
|
|
571
954
|
const mirrorFreshImportGraph = async (
|
|
572
955
|
entryPath: string,
|
|
956
|
+
cwd: string,
|
|
573
957
|
mirrorRoot: string
|
|
574
958
|
): Promise<string> => {
|
|
575
959
|
const scanned = new Set<string>();
|
|
576
960
|
const context: MirrorWalkContext = {
|
|
577
961
|
copied: new Set<string>(),
|
|
962
|
+
cwd,
|
|
963
|
+
linkedPackageNames: new Set<string>(),
|
|
578
964
|
mirrorRoot,
|
|
579
965
|
visitedDirectories: new Set<string>(),
|
|
580
966
|
};
|
|
@@ -584,7 +970,7 @@ const mirrorFreshImportGraph = async (
|
|
|
584
970
|
return;
|
|
585
971
|
}
|
|
586
972
|
scanned.add(modulePath);
|
|
587
|
-
await scanAndVisitLocalImports(modulePath, visit);
|
|
973
|
+
await scanAndVisitLocalImports(modulePath, context, visit);
|
|
588
974
|
await mirrorImportedModule(modulePath, context);
|
|
589
975
|
};
|
|
590
976
|
|
|
@@ -620,9 +1006,14 @@ const prepareMirror = async (
|
|
|
620
1006
|
absolutePath: string,
|
|
621
1007
|
cwd: string
|
|
622
1008
|
): Promise<{ mirrorRoot: string; freshPath: string }> => {
|
|
623
|
-
const
|
|
1009
|
+
const resolvedCwd = resolve(cwd);
|
|
1010
|
+
const mirrorRoot = freshMirrorRootPath(resolvedCwd);
|
|
624
1011
|
try {
|
|
625
|
-
const freshPath = await mirrorFreshImportGraph(
|
|
1012
|
+
const freshPath = await mirrorFreshImportGraph(
|
|
1013
|
+
absolutePath,
|
|
1014
|
+
resolvedCwd,
|
|
1015
|
+
mirrorRoot
|
|
1016
|
+
);
|
|
626
1017
|
return { freshPath, mirrorRoot };
|
|
627
1018
|
} catch (error) {
|
|
628
1019
|
removeLoadAppMirrorRootQuietly(mirrorRoot);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `release.check` trail -- Branch-local release rule evaluation.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Result, trail, ValidationError } from '@ontrails/core';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
import { runReleaseCheck } from '../release/check.js';
|
|
9
|
+
import { resolveTrailRootDir } from './root-dir.js';
|
|
10
|
+
|
|
11
|
+
const releaseCheckInputSchema = z.object({
|
|
12
|
+
baseRef: z
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('Base git ref for changed-file and contract fact comparison'),
|
|
16
|
+
changedFiles: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('Path to a newline-delimited changed-file list'),
|
|
20
|
+
configPath: z.string().optional().describe('Path to trails.config.ts'),
|
|
21
|
+
releaseNone: z
|
|
22
|
+
.boolean()
|
|
23
|
+
.default(false)
|
|
24
|
+
.describe('Compatibility no-release override'),
|
|
25
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const contractReleaseFactAspectSchema = z.enum([
|
|
29
|
+
'input',
|
|
30
|
+
'output',
|
|
31
|
+
'surfaces',
|
|
32
|
+
'trail',
|
|
33
|
+
'visibility',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const contractReleaseFactSchema = z.object({
|
|
37
|
+
aspect: contractReleaseFactAspectSchema,
|
|
38
|
+
baseHash: z.string().nullable(),
|
|
39
|
+
changedFiles: z.array(z.string()).readonly(),
|
|
40
|
+
currentHash: z.string().nullable(),
|
|
41
|
+
packageName: z.string().optional(),
|
|
42
|
+
path: z.string(),
|
|
43
|
+
trailId: z.string(),
|
|
44
|
+
workspacePath: z.string().optional(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const releaseCheckOutputSchema = z.object({
|
|
48
|
+
affectedPackages: z.array(z.string()).readonly(),
|
|
49
|
+
changedChangesets: z.array(z.string()).readonly(),
|
|
50
|
+
configPath: z.string().optional(),
|
|
51
|
+
contractFacts: z.array(contractReleaseFactSchema).readonly(),
|
|
52
|
+
coveredPackages: z.array(z.string()).readonly(),
|
|
53
|
+
errors: z.array(z.string()).readonly(),
|
|
54
|
+
formatted: z.string(),
|
|
55
|
+
matchedRuleIds: z.array(z.string()).readonly(),
|
|
56
|
+
noReleaseOverride: z.boolean(),
|
|
57
|
+
passed: z.boolean(),
|
|
58
|
+
releaseNone: z.boolean(),
|
|
59
|
+
uncoveredContractFacts: z.array(contractReleaseFactSchema).readonly(),
|
|
60
|
+
versionRelease: z.boolean(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const releaseCheckTrail = trail('release.check', {
|
|
64
|
+
blaze: async (input, ctx) => {
|
|
65
|
+
const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
|
|
66
|
+
if (rootDirResult.isErr()) {
|
|
67
|
+
return rootDirResult;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
return Result.ok(
|
|
72
|
+
await runReleaseCheck({
|
|
73
|
+
...(input.baseRef === undefined ? {} : { baseRef: input.baseRef }),
|
|
74
|
+
...(input.changedFiles === undefined
|
|
75
|
+
? {}
|
|
76
|
+
: { changedFilesPath: input.changedFiles }),
|
|
77
|
+
...(input.configPath === undefined
|
|
78
|
+
? {}
|
|
79
|
+
: { configPath: input.configPath }),
|
|
80
|
+
env: ctx.env ?? {},
|
|
81
|
+
releaseNone: input.releaseNone,
|
|
82
|
+
repoRoot: rootDirResult.value,
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return Result.err(
|
|
87
|
+
new ValidationError(
|
|
88
|
+
error instanceof Error ? error.message : String(error)
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
description: 'Check branch-local release rules',
|
|
94
|
+
examples: [
|
|
95
|
+
{
|
|
96
|
+
input: { baseRef: 'HEAD' },
|
|
97
|
+
name: 'Check release rules from the current HEAD',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
input: releaseCheckInputSchema,
|
|
101
|
+
intent: 'read',
|
|
102
|
+
output: releaseCheckOutputSchema,
|
|
103
|
+
permit: 'public',
|
|
104
|
+
});
|