breakage 0.9.0 → 0.9.1
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/README.md +1 -1
- package/dist/{breakage-DIY49fzP.js → breakage-DJiIiLzL.js} +632 -9
- package/dist/breakage-DJiIiLzL.js.map +1 -0
- package/dist/breakage.d.ts +198 -86
- package/dist/breakage.d.ts.map +1 -1
- package/dist/breakage.js +2 -2
- package/dist/breakage.x.js +142 -3
- package/dist/breakage.x.js.map +1 -1
- package/npm.md +144 -12
- package/package.json +1 -1
- package/src/breakage.ts +17 -0
- package/src/breakage.x.ts +26 -0
- package/src/config.ts +3 -0
- package/src/npm/check.ts +19 -0
- package/src/npm/cli.ts +4 -0
- package/src/npm/matrix-cli.ts +170 -0
- package/src/npm/matrix-model.ts +395 -0
- package/src/npm/matrix.ts +657 -0
- package/dist/breakage-DIY49fzP.js.map +0 -1
|
@@ -6,10 +6,10 @@ import { simpleGit } from "simple-git";
|
|
|
6
6
|
import logger from "takua";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import * as fs from "node:fs/promises";
|
|
9
|
-
import { chmod, copyFile, lstat, mkdir, readFile, readdir, realpath, rename, rm, rmdir, writeFile } from "node:fs/promises";
|
|
9
|
+
import { chmod, copyFile, glob, lstat, mkdir, readFile, readdir, realpath, rename, rm, rmdir, writeFile } from "node:fs/promises";
|
|
10
10
|
import semver from "semver";
|
|
11
11
|
import { type } from "arktype";
|
|
12
|
-
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
13
|
import { setTimeout } from "node:timers/promises";
|
|
14
14
|
import { existsSync } from "node:fs";
|
|
15
15
|
import { cli, options } from "comline";
|
|
@@ -455,6 +455,235 @@ function validateConfig(value) {
|
|
|
455
455
|
function validatePeerRanges(peers) {
|
|
456
456
|
for (const range of Object.values(peers)) eras(range);
|
|
457
457
|
}
|
|
458
|
+
// src/npm/matrix-model.ts
|
|
459
|
+
const npmMatrixConfigSchema = type({
|
|
460
|
+
"+": `reject`,
|
|
461
|
+
configs: `string | string[]`,
|
|
462
|
+
prepare: type(`string`).or(type(`Function`).as()),
|
|
463
|
+
"label?": type(`Function`).as()
|
|
464
|
+
});
|
|
465
|
+
const transition = type({
|
|
466
|
+
previousVersion: `string`,
|
|
467
|
+
version: `string`,
|
|
468
|
+
delta: `'major' | 'premajor' | 'minor' | 'preminor' | 'patch' | 'prepatch' | 'prerelease' | 'release' | 'none'`
|
|
469
|
+
});
|
|
470
|
+
const identity = type({
|
|
471
|
+
version: `string`,
|
|
472
|
+
source: `'installed' | 'candidate'`
|
|
473
|
+
});
|
|
474
|
+
const reportSchema = type({
|
|
475
|
+
package: {
|
|
476
|
+
name: `string`,
|
|
477
|
+
version: `string`
|
|
478
|
+
},
|
|
479
|
+
mode: `'check' | 'test' | 'coverage-only'`,
|
|
480
|
+
exitCode: `0 | 1 | 2`,
|
|
481
|
+
errors: `string[]`,
|
|
482
|
+
coverage: type({
|
|
483
|
+
peer: `string`,
|
|
484
|
+
era: `string`,
|
|
485
|
+
representatives: `string[]`
|
|
486
|
+
}).array(),
|
|
487
|
+
support: {
|
|
488
|
+
status: `'unchanged' | 'breaking-release' | 'end-of-support' | 'first-release' | 'not-checked' | 'setup-error'`,
|
|
489
|
+
changes: type({
|
|
490
|
+
peer: `string`,
|
|
491
|
+
reason: `'narrowed-range' | 'required-peer'`,
|
|
492
|
+
previous: `string | null`,
|
|
493
|
+
current: `string | null`
|
|
494
|
+
}).array(),
|
|
495
|
+
"previousVersion?": `string`,
|
|
496
|
+
"release?": transition.and({ breaking: `boolean` }),
|
|
497
|
+
"error?": `string`
|
|
498
|
+
},
|
|
499
|
+
representatives: type({
|
|
500
|
+
path: `string`,
|
|
501
|
+
status: `'passed' | 'audited' | 'skipped' | 'compatibility-failure' | 'setup-error'`,
|
|
502
|
+
peers: { "[string]": identity.and({
|
|
503
|
+
path: `string`,
|
|
504
|
+
"archiveVersion?": `string`
|
|
505
|
+
}) },
|
|
506
|
+
suites: type({
|
|
507
|
+
peer: `string`,
|
|
508
|
+
status: `'passed' | 'compatibility-failure' | 'setup-error' | 'not-run'`,
|
|
509
|
+
"output?": `string`
|
|
510
|
+
}).array(),
|
|
511
|
+
warnings: `string[]`,
|
|
512
|
+
"error?": `string`
|
|
513
|
+
}).array()
|
|
514
|
+
}).as();
|
|
515
|
+
const npmPlanSchema = type({
|
|
516
|
+
"+": `reject`,
|
|
517
|
+
formatVersion: `1`,
|
|
518
|
+
id: `string`,
|
|
519
|
+
runId: `string`,
|
|
520
|
+
commit: `string`,
|
|
521
|
+
configs: `string[]`,
|
|
522
|
+
packages: type({
|
|
523
|
+
config: `string`,
|
|
524
|
+
directory: `string`,
|
|
525
|
+
name: `string`,
|
|
526
|
+
configDigest: `string`,
|
|
527
|
+
inputDigest: `string`,
|
|
528
|
+
audit: reportSchema,
|
|
529
|
+
releases: type({
|
|
530
|
+
context: {
|
|
531
|
+
packageName: `string`,
|
|
532
|
+
packageDirectory: `string`,
|
|
533
|
+
currentVersion: `string`,
|
|
534
|
+
"baseline?": {
|
|
535
|
+
version: `string`,
|
|
536
|
+
"ref?": `string`,
|
|
537
|
+
"commit?": `string`
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
"transition?": transition
|
|
541
|
+
}).array()
|
|
542
|
+
}).array(),
|
|
543
|
+
environments: type({
|
|
544
|
+
id: `string`,
|
|
545
|
+
label: `string`,
|
|
546
|
+
packageName: `string`,
|
|
547
|
+
representative: `string`,
|
|
548
|
+
suites: `string[]`,
|
|
549
|
+
peers: { "[string]": identity }
|
|
550
|
+
}).array(),
|
|
551
|
+
archives: type({
|
|
552
|
+
path: `string`,
|
|
553
|
+
file: `string`,
|
|
554
|
+
sha256: `string`
|
|
555
|
+
}).array()
|
|
556
|
+
});
|
|
557
|
+
const npmReceiptSchema = type({
|
|
558
|
+
"+": `reject`,
|
|
559
|
+
formatVersion: `1`,
|
|
560
|
+
planId: `string`,
|
|
561
|
+
id: `string`,
|
|
562
|
+
exitCode: `0 | 1 | 2`,
|
|
563
|
+
"report?": reportSchema,
|
|
564
|
+
"error?": `string`
|
|
565
|
+
});
|
|
566
|
+
/** Stable object ordering; arrays retain their semantic order. */
|
|
567
|
+
function canonical(value) {
|
|
568
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(`,`)}]`;
|
|
569
|
+
if (value && typeof value === `object`) return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(`,`)}}`;
|
|
570
|
+
return JSON.stringify(value) ?? `undefined`;
|
|
571
|
+
}
|
|
572
|
+
function digest(value) {
|
|
573
|
+
return createHash(`sha256`).update(value).digest(`hex`);
|
|
574
|
+
}
|
|
575
|
+
function planDigest(plan) {
|
|
576
|
+
return digest(canonical(plan));
|
|
577
|
+
}
|
|
578
|
+
function peerIdentities(peers) {
|
|
579
|
+
return Object.fromEntries(Object.entries(peers).map(([name, { version, source }]) => [name, {
|
|
580
|
+
version,
|
|
581
|
+
source
|
|
582
|
+
}]));
|
|
583
|
+
}
|
|
584
|
+
function requireMatrix(condition, message) {
|
|
585
|
+
if (!condition) throw new Error(message);
|
|
586
|
+
}
|
|
587
|
+
function sameNames(actual, expected, message) {
|
|
588
|
+
const counts = (names) => {
|
|
589
|
+
const result = /* @__PURE__ */ new Map();
|
|
590
|
+
for (const name of names) result.set(name, (result.get(name) ?? 0) + 1);
|
|
591
|
+
return result;
|
|
592
|
+
};
|
|
593
|
+
const actualCounts = counts(actual);
|
|
594
|
+
const expectedCounts = counts(expected);
|
|
595
|
+
const missing = [...expectedCounts].filter(([name, count]) => (actualCounts.get(name) ?? 0) < count).map(([name]) => name);
|
|
596
|
+
const extra = [...actualCounts].filter(([name, count]) => count > (expectedCounts.get(name) ?? 0)).map(([name]) => name);
|
|
597
|
+
requireMatrix(!missing.length && !extra.length, `${message}${missing.length ? ` Missing: ${missing.join(`, `)}.` : ``}${extra.length ? ` Unexpected or duplicated: ${extra.join(`, `)}.` : ``}`);
|
|
598
|
+
}
|
|
599
|
+
/** Validate the audit as well as the plan envelope; empty or partial plans never pass. */
|
|
600
|
+
function validateNpmPlan(value) {
|
|
601
|
+
const plan = npmPlanSchema.assert(value);
|
|
602
|
+
const { id, ...payload } = plan;
|
|
603
|
+
requireMatrix(id === planDigest(payload), `Plan digest does not match its contents.`);
|
|
604
|
+
requireMatrix(plan.runId.length > 0 && /^[a-f0-9]{40,64}$/.test(plan.commit), `Invalid plan source identity.`);
|
|
605
|
+
requireMatrix(plan.configs.length && plan.packages.length && plan.environments.length && plan.archives.length, `Plan must contain packages, environments, and archives.`);
|
|
606
|
+
for (const [name, values] of [
|
|
607
|
+
[`package`, plan.packages.map((p) => p.name)],
|
|
608
|
+
[`configuration`, plan.packages.map((p) => p.config)],
|
|
609
|
+
[`environment`, plan.environments.map((e) => e.id)],
|
|
610
|
+
[`archive`, plan.archives.map((a) => a.path)]
|
|
611
|
+
]) requireMatrix(new Set(values).size === values.length, `Duplicate ${name} identities in plan.`);
|
|
612
|
+
for (const archive of plan.archives) requireMatrix(/^[a-f0-9]{64}$/.test(archive.sha256) && archive.file === `archives/${archive.sha256}.tgz`, `Invalid archive digest or bundle path: ${archive.path}.`);
|
|
613
|
+
sameNames([...new Set(plan.environments.map((e) => e.packageName))], plan.packages.map((p) => p.name), `Plan environments do not match package audits.`);
|
|
614
|
+
for (const pkg of plan.packages) {
|
|
615
|
+
const audit = pkg.audit;
|
|
616
|
+
requireMatrix(audit.package.name === pkg.name && audit.mode === `coverage-only` && audit.exitCode === 0 && audit.errors.length === 0 && !audit.support.error, `Coverage audit failed for ${pkg.name}.`);
|
|
617
|
+
requireMatrix([
|
|
618
|
+
`unchanged`,
|
|
619
|
+
`breaking-release`,
|
|
620
|
+
`first-release`
|
|
621
|
+
].includes(audit.support.status), `Support audit did not pass for ${pkg.name}.`);
|
|
622
|
+
requireMatrix(audit.coverage.length && audit.coverage.every((c) => c.representatives.length), `Uncovered peer era for ${pkg.name}.`);
|
|
623
|
+
const expected = plan.environments.filter((e) => e.packageName === pkg.name);
|
|
624
|
+
requireMatrix(new Set(expected.map((e) => e.representative)).size === expected.length, `Duplicate representative paths for ${pkg.name}.`);
|
|
625
|
+
sameNames(audit.representatives.map((r) => r.path), expected.map((e) => e.representative), `Missing or duplicate audited environments for ${pkg.name}.`);
|
|
626
|
+
for (const env of expected) {
|
|
627
|
+
const result = audit.representatives.find((r) => r.path === env.representative);
|
|
628
|
+
requireMatrix(result.status === `audited` && !result.error && env.suites.length, `Environment was not audited: ${pkg.name} / ${env.representative}.`);
|
|
629
|
+
sameNames(result.suites.map((s) => s.peer), env.suites, `Audited suites differ: ${env.id}.`);
|
|
630
|
+
requireMatrix(new Set(env.suites).size === env.suites.length && result.suites.every((s) => s.status === `not-run`), `Invalid audited suites: ${env.id}.`);
|
|
631
|
+
requireMatrix(canonical(peerIdentities(result.peers)) === canonical(env.peers), `Audited peer identities differ: ${env.id}.`);
|
|
632
|
+
}
|
|
633
|
+
for (const coverage of audit.coverage) for (const name of coverage.representatives) requireMatrix(expected.some((e) => e.representative === name && e.suites.includes(coverage.peer)), `Coverage references an unplanned suite: ${pkg.name} / ${name} / ${coverage.peer}.`);
|
|
634
|
+
}
|
|
635
|
+
return plan;
|
|
636
|
+
}
|
|
637
|
+
/** Pure receipt verification; runner-local installation paths are diagnostic only. */
|
|
638
|
+
function verifyNpmReceipts(value, receipts) {
|
|
639
|
+
const errors = [];
|
|
640
|
+
let environments = 0;
|
|
641
|
+
try {
|
|
642
|
+
const plan = validateNpmPlan(value);
|
|
643
|
+
environments = plan.environments.length;
|
|
644
|
+
const parsed = receipts.map((r, i) => {
|
|
645
|
+
try {
|
|
646
|
+
return npmReceiptSchema.assert(r);
|
|
647
|
+
} catch (cause) {
|
|
648
|
+
throw new Error(`Malformed receipt ${i + 1}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
const environmentName = (id) => {
|
|
652
|
+
const env = plan.environments.find((e) => e.id === id);
|
|
653
|
+
return env ? `${env.packageName} / ${env.representative} (${id})` : id;
|
|
654
|
+
};
|
|
655
|
+
sameNames(parsed.map((r) => environmentName(r.id)), plan.environments.map((e) => environmentName(e.id)), `Missing, duplicate, or unexpected environment receipts.`);
|
|
656
|
+
for (const env of plan.environments) try {
|
|
657
|
+
const receipt = parsed.find((r) => r.id === env.id);
|
|
658
|
+
requireMatrix(receipt.planId === plan.id, `Receipt belongs to a different plan.`);
|
|
659
|
+
requireMatrix(receipt.exitCode === 0 && !receipt.error && receipt.report, receipt.error ?? `Environment execution failed.`);
|
|
660
|
+
const report = receipt.report;
|
|
661
|
+
const pkg = plan.packages.find((p) => p.name === env.packageName);
|
|
662
|
+
requireMatrix(canonical(report.package) === canonical(pkg.audit.package), `Wrong package identity.`);
|
|
663
|
+
requireMatrix(report.mode === `test` && report.exitCode === 0 && report.errors.length === 0 && report.support.status === `not-checked` && !report.support.error, `Receipt does not contain a successful execution report.`);
|
|
664
|
+
sameNames(report.representatives.map((r) => r.path), pkg.audit.representatives.map((r) => r.path), `Missing or duplicate representatives in execution report.`);
|
|
665
|
+
for (const result of report.representatives) {
|
|
666
|
+
if (result.path !== env.representative) {
|
|
667
|
+
requireMatrix(result.status === `skipped` && result.suites.length === 0 && !result.error, `Unexpected execution: ${result.path}.`);
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
requireMatrix(result.status === `passed` && !result.error, `Environment did not execute successfully.`);
|
|
671
|
+
sameNames(result.suites.map((s) => s.peer), env.suites, `Missing, duplicate, or unexpected relationship suites.`);
|
|
672
|
+
for (const suite of result.suites) requireMatrix(suite.status === `passed`, `Suite did not pass: ${suite.peer}.`);
|
|
673
|
+
requireMatrix(canonical(peerIdentities(result.peers)) === canonical(env.peers), `Installed peer identities differ from the audit.`);
|
|
674
|
+
}
|
|
675
|
+
} catch (error) {
|
|
676
|
+
errors.push(`${env.packageName} / ${env.representative}: ${error instanceof Error ? error.message : String(error)}`);
|
|
677
|
+
}
|
|
678
|
+
} catch (error) {
|
|
679
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
680
|
+
}
|
|
681
|
+
return {
|
|
682
|
+
exitCode: errors.length ? 2 : 0,
|
|
683
|
+
errors,
|
|
684
|
+
environments
|
|
685
|
+
};
|
|
686
|
+
}
|
|
458
687
|
// src/config.ts
|
|
459
688
|
const configFields = {
|
|
460
689
|
tagPattern: type(`string`),
|
|
@@ -477,7 +706,8 @@ const configSchema = type({
|
|
|
477
706
|
"verbose?": configFields.verbose.or(`undefined`),
|
|
478
707
|
"out?": configFields.out,
|
|
479
708
|
"release?": releaseSchema,
|
|
480
|
-
"npm?": npmConfigSchema
|
|
709
|
+
"npm?": npmConfigSchema,
|
|
710
|
+
"npmMatrix?": npmMatrixConfigSchema
|
|
481
711
|
});
|
|
482
712
|
/** The complete config, with no fields exposed to argv by this loader. */
|
|
483
713
|
const configOptions = options(`Breakage configuration`, configSchema, {
|
|
@@ -490,7 +720,8 @@ const configOptions = options(`Breakage configuration`, configSchema, {
|
|
|
490
720
|
verbose: { configOnly: true },
|
|
491
721
|
out: { configOnly: true },
|
|
492
722
|
release: { configOnly: true },
|
|
493
|
-
npm: { configOnly: true }
|
|
723
|
+
npm: { configOnly: true },
|
|
724
|
+
npmMatrix: { configOnly: true }
|
|
494
725
|
});
|
|
495
726
|
/** Share the root release model with npm checks unless they supply their own. */
|
|
496
727
|
function defineConfig(config) {
|
|
@@ -1029,7 +1260,7 @@ async function checkLocalReleases(config, base, manifest, representatives) {
|
|
|
1029
1260
|
}
|
|
1030
1261
|
}))).flat();
|
|
1031
1262
|
}
|
|
1032
|
-
async function runRepresentative(config, base, manifest, directory, coverageOnly, selection) {
|
|
1263
|
+
async function runRepresentative(config, base, manifest, directory, coverageOnly, selection, plannedSuites) {
|
|
1033
1264
|
const result = {
|
|
1034
1265
|
path: path.relative(base, directory).split(path.sep).join(`/`),
|
|
1035
1266
|
status: `skipped`,
|
|
@@ -1052,6 +1283,7 @@ async function runRepresentative(config, base, manifest, directory, coverageOnly
|
|
|
1052
1283
|
const peers = Object.keys(config.suites).filter((peer) => {
|
|
1053
1284
|
const evidence = result.peers[peer];
|
|
1054
1285
|
if (!evidence) return false;
|
|
1286
|
+
if (plannedSuites !== void 0) return plannedSuites.includes(peer);
|
|
1055
1287
|
if (selection && `peer` in selection) return peer === selection.peer;
|
|
1056
1288
|
if (selection && `representative` in selection) return true;
|
|
1057
1289
|
return matches(evidence.version, manifest.peerDependencies[peer]);
|
|
@@ -1086,7 +1318,7 @@ async function runRepresentative(config, base, manifest, directory, coverageOnly
|
|
|
1086
1318
|
return result;
|
|
1087
1319
|
}
|
|
1088
1320
|
}
|
|
1089
|
-
async function run(config, baseDirectory, coverageOnly, selection) {
|
|
1321
|
+
async function run(config, baseDirectory, coverageOnly, selection, plannedSuites) {
|
|
1090
1322
|
const report = {
|
|
1091
1323
|
package: {
|
|
1092
1324
|
name: `(unresolved)`,
|
|
@@ -1139,7 +1371,7 @@ async function run(config, baseDirectory, coverageOnly, selection) {
|
|
|
1139
1371
|
} } : {}
|
|
1140
1372
|
};
|
|
1141
1373
|
const directories = await discover(config, base);
|
|
1142
|
-
const [support, representatives] = await Promise.all([selection ? Promise.resolve(report.support) : checkSupport(effective, base, manifest), Promise.all(directories.map((directory) => runRepresentative(effective, base, manifest, directory, coverageOnly, selection)))]);
|
|
1374
|
+
const [support, representatives] = await Promise.all([selection ? Promise.resolve(report.support) : checkSupport(effective, base, manifest), Promise.all(directories.map((directory) => runRepresentative(effective, base, manifest, directory, coverageOnly, selection, plannedSuites)))]);
|
|
1143
1375
|
report.support = support;
|
|
1144
1376
|
report.representatives = representatives;
|
|
1145
1377
|
if (!selection) {
|
|
@@ -1168,6 +1400,10 @@ async function npmCheck(config, options = {}) {
|
|
|
1168
1400
|
async function npmTest(config, selection, options = {}) {
|
|
1169
1401
|
return await run(config, options.baseDirectory ?? process.cwd(), false, selection);
|
|
1170
1402
|
}
|
|
1403
|
+
/** Internal matrix entrypoint: keep the full config for validation and peer identity checks. */
|
|
1404
|
+
async function npmTestPlanned(config, selection, options = {}) {
|
|
1405
|
+
return await run(config, options.baseDirectory ?? process.cwd(), false, { representative: selection.representative }, selection.suites);
|
|
1406
|
+
}
|
|
1171
1407
|
function formatNpmReport(report) {
|
|
1172
1408
|
const lines = [`${report.package.name}@${report.package.version} — npm ${report.mode}`];
|
|
1173
1409
|
for (const result of report.representatives) {
|
|
@@ -1188,6 +1424,393 @@ function formatNpmReport(report) {
|
|
|
1188
1424
|
if (report.mode === `coverage-only`) lines.push(`Coverage audit only; compatibility assertions were not executed.`);
|
|
1189
1425
|
return `${lines.join(`\n`)}\n`;
|
|
1190
1426
|
}
|
|
1427
|
+
// src/npm/matrix.ts
|
|
1428
|
+
function relative(root, filename) {
|
|
1429
|
+
const result = path.relative(root, path.resolve(filename)).split(path.sep).join(`/`) || `.`;
|
|
1430
|
+
requireMatrix(!result.startsWith(`../`) && result !== `..` && !path.isAbsolute(result), `Matrix input escapes the source checkout: ${filename}.`);
|
|
1431
|
+
return result;
|
|
1432
|
+
}
|
|
1433
|
+
function portablePath(value) {
|
|
1434
|
+
requireMatrix(value.length && !value.includes(`\\`) && !path.posix.isAbsolute(value) && !/^[A-Za-z]:/.test(value) && !value.split(`/`).includes(`..`), `Invalid portable path: ${value}.`);
|
|
1435
|
+
}
|
|
1436
|
+
async function contained(root, filename) {
|
|
1437
|
+
portablePath(filename);
|
|
1438
|
+
const absolute = path.resolve(root, filename);
|
|
1439
|
+
let ancestor = absolute;
|
|
1440
|
+
for (;;) try {
|
|
1441
|
+
await lstat(ancestor);
|
|
1442
|
+
break;
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
if (error.code !== `ENOENT`) throw error;
|
|
1445
|
+
ancestor = path.dirname(ancestor);
|
|
1446
|
+
}
|
|
1447
|
+
requireMatrix(within(await realpath(root), await realpath(ancestor)), `Path escapes its root through a symlink: ${filename}.`);
|
|
1448
|
+
return absolute;
|
|
1449
|
+
}
|
|
1450
|
+
async function fileDigest(filename) {
|
|
1451
|
+
requireMatrix((await lstat(filename)).isFile(), `Expected a regular file: ${filename}.`);
|
|
1452
|
+
return digest(await readFile(filename));
|
|
1453
|
+
}
|
|
1454
|
+
async function sourceIdentity(root) {
|
|
1455
|
+
const git = simpleGit(root);
|
|
1456
|
+
requireMatrix(await realpath(await git.revparse([`--show-toplevel`])) === await realpath(root), `Matrix rootDirectory must be the Git checkout root.`);
|
|
1457
|
+
requireMatrix(!(await git.status()).files.length, `Matrix planning and execution require a clean checkout, including untracked files. Ignore generated archives, bundles, and receipts.`);
|
|
1458
|
+
return await git.revparse([`HEAD`]);
|
|
1459
|
+
}
|
|
1460
|
+
async function ignoredOutput(root, filename) {
|
|
1461
|
+
const git = simpleGit(root);
|
|
1462
|
+
requireMatrix(!(await git.raw([
|
|
1463
|
+
`ls-files`,
|
|
1464
|
+
`--`,
|
|
1465
|
+
filename
|
|
1466
|
+
])).trim() && (await git.raw([
|
|
1467
|
+
`check-ignore`,
|
|
1468
|
+
`--`,
|
|
1469
|
+
filename
|
|
1470
|
+
])).trim(), `Matrix output must be gitignored and untracked: ${filename}.`);
|
|
1471
|
+
}
|
|
1472
|
+
async function discoverConfigs(root, patterns) {
|
|
1473
|
+
const files = /* @__PURE__ */ new Set();
|
|
1474
|
+
for (const pattern of patterns) portablePath(pattern);
|
|
1475
|
+
for await (const filename of glob(patterns, {
|
|
1476
|
+
cwd: root,
|
|
1477
|
+
exclude: [
|
|
1478
|
+
`**/node_modules/**`,
|
|
1479
|
+
`**/.git/**`,
|
|
1480
|
+
`**/test/**`
|
|
1481
|
+
]
|
|
1482
|
+
})) {
|
|
1483
|
+
const name = relative(root, path.resolve(root, filename));
|
|
1484
|
+
if ((await loadConfig(await contained(root, name))).npm) files.add(name);
|
|
1485
|
+
}
|
|
1486
|
+
requireMatrix(files.size, `No npm configurations matched the matrix configs.`);
|
|
1487
|
+
return [...files].sort();
|
|
1488
|
+
}
|
|
1489
|
+
function configDigest(config, root, base) {
|
|
1490
|
+
return digest(canonical({
|
|
1491
|
+
...config,
|
|
1492
|
+
packageDirectory: relative(root, base),
|
|
1493
|
+
artifact: relative(root, path.resolve(base, config.artifact)),
|
|
1494
|
+
baseline: `baselineFile` in config.baseline && config.baseline.baselineFile ? {
|
|
1495
|
+
...config.baseline,
|
|
1496
|
+
baselineFile: relative(root, path.resolve(base, config.baseline.baselineFile))
|
|
1497
|
+
} : config.baseline,
|
|
1498
|
+
localPeers: Object.fromEntries(Object.entries(config.localPeers ?? {}).map(([name, peer]) => [name, {
|
|
1499
|
+
directory: relative(root, path.resolve(base, peer.directory)),
|
|
1500
|
+
artifact: relative(root, path.resolve(base, peer.artifact))
|
|
1501
|
+
}])),
|
|
1502
|
+
release: config.release ? {
|
|
1503
|
+
resolveDelta: !!config.release.resolveDelta,
|
|
1504
|
+
policy: !!config.release.policy
|
|
1505
|
+
} : void 0
|
|
1506
|
+
}));
|
|
1507
|
+
}
|
|
1508
|
+
async function packageConfig(root, filename) {
|
|
1509
|
+
const config = (await loadConfig(await contained(root, filename))).npm;
|
|
1510
|
+
validateConfig(config);
|
|
1511
|
+
const baseDirectory = path.dirname(path.resolve(root, filename));
|
|
1512
|
+
const base = path.resolve(baseDirectory, config.packageDirectory ?? `.`);
|
|
1513
|
+
await contained(root, relative(root, base));
|
|
1514
|
+
return {
|
|
1515
|
+
config,
|
|
1516
|
+
base,
|
|
1517
|
+
baseDirectory
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
function portableContext(root, context) {
|
|
1521
|
+
return {
|
|
1522
|
+
...context,
|
|
1523
|
+
packageDirectory: relative(root, context.packageDirectory)
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
async function archiveRequests(root, config, base) {
|
|
1527
|
+
const manifest = await readManifest(path.join(base, `package.json`));
|
|
1528
|
+
const requests = [{
|
|
1529
|
+
packageName: manifest.name,
|
|
1530
|
+
directory: relative(root, base),
|
|
1531
|
+
artifact: relative(root, path.resolve(base, config.artifact))
|
|
1532
|
+
}];
|
|
1533
|
+
for (const [packageName, peer] of Object.entries(config.localPeers ?? {})) {
|
|
1534
|
+
const directory = relative(root, path.resolve(base, peer.directory));
|
|
1535
|
+
try {
|
|
1536
|
+
await lstat(await contained(root, directory));
|
|
1537
|
+
} catch (error) {
|
|
1538
|
+
if (error.code === `ENOENT` && !manifest.peerDependencies?.[packageName]) continue;
|
|
1539
|
+
throw error;
|
|
1540
|
+
}
|
|
1541
|
+
requests.push({
|
|
1542
|
+
packageName,
|
|
1543
|
+
directory,
|
|
1544
|
+
artifact: relative(root, path.resolve(base, peer.artifact))
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
return requests;
|
|
1548
|
+
}
|
|
1549
|
+
async function inputDigest(root, filename, config, base) {
|
|
1550
|
+
const files = /* @__PURE__ */ new Set([filename, relative(root, path.join(base, `package.json`))]);
|
|
1551
|
+
const directories = await discover(config, base);
|
|
1552
|
+
for (const directory of directories) for (const name of [`package.json`, config.lockfile]) files.add(relative(root, path.join(directory, name)));
|
|
1553
|
+
for (const request of await archiveRequests(root, config, base)) files.add(relative(root, path.resolve(root, request.directory, `package.json`)));
|
|
1554
|
+
if (`baselineFile` in config.baseline && config.baseline.baselineFile) files.add(relative(root, path.resolve(base, config.baseline.baselineFile)));
|
|
1555
|
+
const exclude = [
|
|
1556
|
+
`**/node_modules/**`,
|
|
1557
|
+
`**/.git/**`,
|
|
1558
|
+
...directories.flatMap((directory) => {
|
|
1559
|
+
const stage = relative(base, path.join(directory, `test`));
|
|
1560
|
+
return [stage, `${stage}/**`];
|
|
1561
|
+
})
|
|
1562
|
+
];
|
|
1563
|
+
for (const suite of Object.values(config.suites)) for (const pattern of suite.files) {
|
|
1564
|
+
portablePath(pattern);
|
|
1565
|
+
let count = 0;
|
|
1566
|
+
for await (const file of glob(pattern, {
|
|
1567
|
+
cwd: base,
|
|
1568
|
+
exclude
|
|
1569
|
+
})) {
|
|
1570
|
+
const absolute = path.resolve(base, file);
|
|
1571
|
+
if ((await lstat(absolute)).isDirectory()) continue;
|
|
1572
|
+
files.add(relative(root, absolute));
|
|
1573
|
+
count++;
|
|
1574
|
+
}
|
|
1575
|
+
requireMatrix(count, `Suite pattern matched no files: ${pattern}.`);
|
|
1576
|
+
}
|
|
1577
|
+
return digest(canonical(await Promise.all([...files].sort().map(async (file) => [file, await fileDigest(await contained(root, file))]))));
|
|
1578
|
+
}
|
|
1579
|
+
/** Audit all configured packages and prepare one immutable, portable archive bundle. */
|
|
1580
|
+
async function npmPlan(config, options = {}) {
|
|
1581
|
+
npmMatrixConfigSchema.assert(config);
|
|
1582
|
+
const root = path.resolve(options.rootDirectory ?? process.cwd());
|
|
1583
|
+
const output = options.bundleDirectory ?? `.breakage/npm-plan`;
|
|
1584
|
+
const bundle = await contained(root, relative(root, path.resolve(root, output)));
|
|
1585
|
+
await ignoredOutput(root, path.join(bundle, `plan.json`));
|
|
1586
|
+
await mkdir(bundle, { recursive: true });
|
|
1587
|
+
return await withDirectoryLock(path.join(bundle, `.lock`), async () => {
|
|
1588
|
+
await rm(path.join(bundle, `plan.json`), { force: true });
|
|
1589
|
+
const commit = await sourceIdentity(root);
|
|
1590
|
+
const patterns = typeof config.configs === `string` ? [config.configs] : config.configs;
|
|
1591
|
+
const files = await discoverConfigs(root, patterns);
|
|
1592
|
+
const packages = [];
|
|
1593
|
+
const environments = [];
|
|
1594
|
+
const requests = /* @__PURE__ */ new Map();
|
|
1595
|
+
for (const filename of files) {
|
|
1596
|
+
const { config: npm, base, baseDirectory } = await packageConfig(root, filename);
|
|
1597
|
+
const manifest = await readManifest(path.join(base, `package.json`));
|
|
1598
|
+
for (const peer of Object.keys(manifest.peerDependencies ?? {})) requireMatrix(npm.suites[peer] || manifest.peerDependenciesMeta?.[peer]?.optional, `Missing relationship suite for ${manifest.name}: ${peer}.`);
|
|
1599
|
+
const releases = [];
|
|
1600
|
+
const audit = await npmCheck({
|
|
1601
|
+
...npm,
|
|
1602
|
+
...npm.release ? { release: {
|
|
1603
|
+
...npm.release,
|
|
1604
|
+
resolveDelta: async (context) => {
|
|
1605
|
+
const transition = await resolveReleaseDelta(npm.release, context);
|
|
1606
|
+
releases.push({
|
|
1607
|
+
context: portableContext(root, context),
|
|
1608
|
+
...transition ? { transition } : {}
|
|
1609
|
+
});
|
|
1610
|
+
return transition;
|
|
1611
|
+
}
|
|
1612
|
+
} } : {}
|
|
1613
|
+
}, {
|
|
1614
|
+
baseDirectory,
|
|
1615
|
+
coverageOnly: true
|
|
1616
|
+
});
|
|
1617
|
+
requireMatrix(audit.exitCode === 0, `Cannot plan ${manifest.name}:\n${formatNpmReport(audit)}`);
|
|
1618
|
+
for (const result of audit.representatives) {
|
|
1619
|
+
requireMatrix(result.status === `audited` && result.suites.length, `Environment has no executable audited suites: ${manifest.name} / ${result.path}.`);
|
|
1620
|
+
const environment = JSON.parse(await readFile(path.join(base, result.path, `package.json`), `utf8`));
|
|
1621
|
+
const label = config.label?.({
|
|
1622
|
+
packageName: manifest.name,
|
|
1623
|
+
representative: result.path,
|
|
1624
|
+
peers: result.peers,
|
|
1625
|
+
...typeof environment.description === `string` ? { description: environment.description } : {}
|
|
1626
|
+
}) ?? `${manifest.name}: ${result.path}`;
|
|
1627
|
+
requireMatrix(typeof label === `string` && label.trim(), `Invalid label for ${manifest.name} / ${result.path}.`);
|
|
1628
|
+
environments.push({
|
|
1629
|
+
id: digest(`${filename}\0${result.path}`).slice(0, 24),
|
|
1630
|
+
label,
|
|
1631
|
+
packageName: manifest.name,
|
|
1632
|
+
representative: result.path,
|
|
1633
|
+
suites: result.suites.map((s) => s.peer).sort(),
|
|
1634
|
+
peers: peerIdentities(result.peers)
|
|
1635
|
+
});
|
|
1636
|
+
for (const peer of Object.values(result.peers)) peer.path = `(runner-local)`;
|
|
1637
|
+
}
|
|
1638
|
+
packages.push({
|
|
1639
|
+
config: filename,
|
|
1640
|
+
directory: relative(root, base),
|
|
1641
|
+
name: manifest.name,
|
|
1642
|
+
configDigest: configDigest(npm, root, base),
|
|
1643
|
+
inputDigest: await inputDigest(root, filename, npm, base),
|
|
1644
|
+
audit,
|
|
1645
|
+
releases: releases.sort((a, b) => canonical(a.context).localeCompare(canonical(b.context)))
|
|
1646
|
+
});
|
|
1647
|
+
for (const request of await archiveRequests(root, npm, base)) {
|
|
1648
|
+
const previous = requests.get(request.artifact);
|
|
1649
|
+
requireMatrix(!previous || canonical(previous) === canonical(request), `Conflicting archive requests: ${request.artifact}.`);
|
|
1650
|
+
requests.set(request.artifact, request);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
const archives = [...requests.values()].sort((a, b) => a.artifact.localeCompare(b.artifact));
|
|
1654
|
+
for (const request of archives) {
|
|
1655
|
+
await contained(root, request.directory);
|
|
1656
|
+
await contained(root, request.artifact);
|
|
1657
|
+
await ignoredOutput(root, path.resolve(root, request.artifact));
|
|
1658
|
+
}
|
|
1659
|
+
if (typeof config.prepare === `string`) await prepare(config.prepare, root);
|
|
1660
|
+
else await config.prepare({
|
|
1661
|
+
rootDirectory: root,
|
|
1662
|
+
archives
|
|
1663
|
+
});
|
|
1664
|
+
const packed = [];
|
|
1665
|
+
for (const archive of archives) {
|
|
1666
|
+
const filename = await contained(root, archive.artifact);
|
|
1667
|
+
const sha256 = await fileDigest(filename);
|
|
1668
|
+
const file = `archives/${sha256}.tgz`;
|
|
1669
|
+
await mkdir(path.join(bundle, `archives`), { recursive: true });
|
|
1670
|
+
await copyFile(filename, await contained(bundle, file));
|
|
1671
|
+
packed.push({
|
|
1672
|
+
path: archive.artifact,
|
|
1673
|
+
file,
|
|
1674
|
+
sha256
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
requireMatrix(await sourceIdentity(root) === commit, `Source changed while preparing the matrix.`);
|
|
1678
|
+
const payload = {
|
|
1679
|
+
formatVersion: 1,
|
|
1680
|
+
runId: randomUUID(),
|
|
1681
|
+
commit,
|
|
1682
|
+
configs: patterns,
|
|
1683
|
+
packages,
|
|
1684
|
+
environments,
|
|
1685
|
+
archives: packed
|
|
1686
|
+
};
|
|
1687
|
+
const plan = validateNpmPlan({
|
|
1688
|
+
...payload,
|
|
1689
|
+
id: planDigest(payload)
|
|
1690
|
+
});
|
|
1691
|
+
await validateWorkspace(plan, {
|
|
1692
|
+
rootDirectory: root,
|
|
1693
|
+
bundleDirectory: bundle
|
|
1694
|
+
});
|
|
1695
|
+
await writeMatrixJson(path.join(bundle, `plan.json`), plan);
|
|
1696
|
+
return plan;
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
async function validateWorkspace(plan, options) {
|
|
1700
|
+
const root = path.resolve(options.rootDirectory ?? process.cwd());
|
|
1701
|
+
const bundle = path.resolve(root, options.bundleDirectory ?? `.breakage/npm-plan`);
|
|
1702
|
+
requireMatrix(await sourceIdentity(root) === plan.commit, `Plan belongs to a different source commit.`);
|
|
1703
|
+
requireMatrix(canonical(await discoverConfigs(root, plan.configs)) === canonical(plan.packages.map((p) => p.config).sort()), `Plan no longer enumerates every configured npm package.`);
|
|
1704
|
+
const configs = /* @__PURE__ */ new Map();
|
|
1705
|
+
const requiredArchives = /* @__PURE__ */ new Set();
|
|
1706
|
+
for (const pkg of plan.packages) {
|
|
1707
|
+
const loaded = await packageConfig(root, pkg.config);
|
|
1708
|
+
const { config, base } = loaded;
|
|
1709
|
+
for (const request of await archiveRequests(root, config, base)) requiredArchives.add(request.artifact);
|
|
1710
|
+
requireMatrix(relative(root, base) === pkg.directory && configDigest(config, root, base) === pkg.configDigest, `Configuration changed for ${pkg.name}.`);
|
|
1711
|
+
requireMatrix(await inputDigest(root, pkg.config, config, base) === pkg.inputDigest, `Contract, manifest, or lockfile inputs changed for ${pkg.name}.`);
|
|
1712
|
+
requireMatrix(canonical((await discover(config, base)).map((d) => relative(base, d)).sort()) === canonical(pkg.audit.representatives.map((r) => r.path).sort()), `Representative discovery changed for ${pkg.name}.`);
|
|
1713
|
+
for (const release of pkg.releases) {
|
|
1714
|
+
requireMatrix(config.release, `Release configuration disappeared for ${pkg.name}.`);
|
|
1715
|
+
requireMatrix(canonical(await resolveReleaseDelta(config.release, {
|
|
1716
|
+
...release.context,
|
|
1717
|
+
packageDirectory: await contained(root, release.context.packageDirectory)
|
|
1718
|
+
})) === canonical(release.transition), `Release evidence changed for ${pkg.name} / ${release.context.packageName}.`);
|
|
1719
|
+
}
|
|
1720
|
+
if (pkg.audit.support.release) {
|
|
1721
|
+
const expected = pkg.audit.support.release;
|
|
1722
|
+
requireMatrix(canonical(versionDelta(expected.previousVersion, expected.version, config.release?.policy)) === canonical(expected), `Release policy changed for ${pkg.name}.`);
|
|
1723
|
+
}
|
|
1724
|
+
configs.set(pkg.name, loaded);
|
|
1725
|
+
}
|
|
1726
|
+
requireMatrix(canonical([...requiredArchives].sort()) === canonical(plan.archives.map((a) => a.path).sort()), `Planned archives do not match the configured packages and local peers.`);
|
|
1727
|
+
for (const archive of plan.archives) {
|
|
1728
|
+
portablePath(archive.path);
|
|
1729
|
+
requireMatrix(archive.file === `archives/${archive.sha256}.tgz`, `Invalid bundled archive path: ${archive.file}.`);
|
|
1730
|
+
requireMatrix(await fileDigest(await contained(bundle, archive.file)) === archive.sha256, `Candidate archive changed: ${archive.path}.`);
|
|
1731
|
+
}
|
|
1732
|
+
return {
|
|
1733
|
+
root,
|
|
1734
|
+
bundle,
|
|
1735
|
+
configs
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
/** Execute exactly one audited environment, producing failure evidence whenever the plan is valid. */
|
|
1739
|
+
async function npmExecute(value, id, options = {}) {
|
|
1740
|
+
const plan = validateNpmPlan(value);
|
|
1741
|
+
const receipt = {
|
|
1742
|
+
formatVersion: 1,
|
|
1743
|
+
planId: plan.id,
|
|
1744
|
+
id,
|
|
1745
|
+
exitCode: 2
|
|
1746
|
+
};
|
|
1747
|
+
try {
|
|
1748
|
+
const env = plan.environments.find((e) => e.id === id);
|
|
1749
|
+
requireMatrix(env, `Unknown planned environment: ${id}.`);
|
|
1750
|
+
const { root, bundle, configs } = await validateWorkspace(plan, options);
|
|
1751
|
+
const { config, baseDirectory } = configs.get(env.packageName);
|
|
1752
|
+
const byPath = new Map(plan.archives.map((a) => [a.path, path.join(bundle, a.file)]));
|
|
1753
|
+
const base = path.resolve(baseDirectory, config.packageDirectory ?? `.`);
|
|
1754
|
+
const artifactFor = (filename) => {
|
|
1755
|
+
const archive = byPath.get(relative(root, path.resolve(base, filename)));
|
|
1756
|
+
requireMatrix(archive, `Plan is missing archive: ${filename}.`);
|
|
1757
|
+
return archive;
|
|
1758
|
+
};
|
|
1759
|
+
receipt.report = await npmTestPlanned({
|
|
1760
|
+
...config,
|
|
1761
|
+
artifact: artifactFor(config.artifact),
|
|
1762
|
+
localPeers: Object.fromEntries(Object.entries(config.localPeers ?? {}).map(([name, peer]) => [name, {
|
|
1763
|
+
...peer,
|
|
1764
|
+
artifact: byPath.get(relative(root, path.resolve(base, peer.artifact))) ?? peer.artifact
|
|
1765
|
+
}]))
|
|
1766
|
+
}, {
|
|
1767
|
+
representative: env.representative,
|
|
1768
|
+
suites: env.suites
|
|
1769
|
+
}, { baseDirectory });
|
|
1770
|
+
receipt.exitCode = receipt.report.exitCode;
|
|
1771
|
+
await validateWorkspace(plan, options);
|
|
1772
|
+
if (receipt.exitCode === 0) {
|
|
1773
|
+
const result = receipt.report.representatives.find((r) => r.path === env.representative);
|
|
1774
|
+
requireMatrix(result?.status === `passed` && canonical(result.suites.map((s) => s.peer).sort()) === canonical(env.suites) && result.suites.every((s) => s.status === `passed`) && canonical(peerIdentities(result.peers)) === canonical(env.peers), `Execution differs from the planned suites or peer identities: ${env.label}.`);
|
|
1775
|
+
}
|
|
1776
|
+
} catch (error) {
|
|
1777
|
+
receipt.exitCode = 2;
|
|
1778
|
+
receipt.error = errorMessage(error);
|
|
1779
|
+
}
|
|
1780
|
+
return receipt;
|
|
1781
|
+
}
|
|
1782
|
+
/** Verify source/config/archive identities and then require all planned execution receipts. */
|
|
1783
|
+
async function npmVerify(value, receipts, options = {}) {
|
|
1784
|
+
try {
|
|
1785
|
+
const plan = validateNpmPlan(value);
|
|
1786
|
+
await validateWorkspace(plan, options);
|
|
1787
|
+
return verifyNpmReceipts(plan, receipts);
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
return {
|
|
1790
|
+
exitCode: 2,
|
|
1791
|
+
errors: [errorMessage(error)],
|
|
1792
|
+
environments: 0
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
function npmMatrix(value) {
|
|
1797
|
+
return { include: validateNpmPlan(value).environments.map(({ id, label, packageName, representative }) => ({
|
|
1798
|
+
id,
|
|
1799
|
+
label,
|
|
1800
|
+
packageName,
|
|
1801
|
+
representative
|
|
1802
|
+
})) };
|
|
1803
|
+
}
|
|
1804
|
+
async function writeMatrixJson(filename, value) {
|
|
1805
|
+
await mkdir(path.dirname(filename), { recursive: true });
|
|
1806
|
+
const temporary = `${filename}.${randomUUID()}.tmp`;
|
|
1807
|
+
try {
|
|
1808
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { flag: `wx` });
|
|
1809
|
+
await rename(temporary, filename);
|
|
1810
|
+
} finally {
|
|
1811
|
+
await rm(temporary, { force: true });
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1191
1814
|
// src/breakage.ts
|
|
1192
1815
|
async function breakage(options) {
|
|
1193
1816
|
rejectCertificationOptions(options);
|
|
@@ -1344,6 +1967,6 @@ async function breakage(options) {
|
|
|
1344
1967
|
logMarks?.();
|
|
1345
1968
|
}
|
|
1346
1969
|
}
|
|
1347
|
-
export {
|
|
1970
|
+
export { resolveConfig as _, npmVerify as a, breakagePrelude as b, npmCheck as c, configOptions as d, configPath as f, loadConfig as g, defineConfig as h, npmPlan as i, npmTest as l, defaultConfig as m, npmExecute as n, writeMatrixJson as o, configSchema as p, npmMatrix as r, formatNpmReport as s, breakage as t, configFields as u, verifyNpmReceipts as v, errorMessage as x, validateConfig as y };
|
|
1348
1971
|
|
|
1349
|
-
//# sourceMappingURL=breakage-
|
|
1972
|
+
//# sourceMappingURL=breakage-DJiIiLzL.js.map
|