breakage 0.8.5 → 0.9.0
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 +163 -47
- package/dist/{breakage-B7dJsgcu.js → breakage-DIY49fzP.js} +331 -217
- package/dist/breakage-DIY49fzP.js.map +1 -0
- package/dist/breakage.d.ts +111 -55
- package/dist/breakage.d.ts.map +1 -1
- package/dist/breakage.js +3 -1
- package/dist/breakage.x.js +78 -80
- package/dist/breakage.x.js.map +1 -1
- package/dist/changesets.d.ts +8 -0
- package/dist/changesets.d.ts.map +1 -0
- package/dist/changesets.js +27 -0
- package/dist/changesets.js.map +1 -0
- package/dist/release-BsdIDT4g.js +69 -0
- package/dist/release-BsdIDT4g.js.map +1 -0
- package/dist/release-D47BqRuB.d.ts +40 -0
- package/dist/release-D47BqRuB.d.ts.map +1 -0
- package/npm.md +55 -33
- package/package.json +45 -6
- package/src/breakage.ts +125 -73
- package/src/breakage.x.ts +148 -127
- package/src/changesets.ts +51 -0
- package/src/config.ts +128 -0
- package/src/npm/check.ts +51 -50
- package/src/npm/cli.ts +26 -22
- package/src/npm/config.ts +38 -6
- package/src/npm/environment.ts +10 -10
- package/src/npm/release.ts +9 -25
- package/src/npm/types.ts +5 -45
- package/src/release.ts +153 -0
- package/src/{npm/command.ts → task.ts} +2 -0
- package/dist/breakage-B7dJsgcu.js.map +0 -1
- package/dist/breakage.$configPath.schema.json +0 -33
- package/dist/breakage.help.schema.json +0 -6
- package/dist/breakage.main.schema.json +0 -33
- package/dist/breakage.npm.check.schema.json +0 -16
- package/dist/breakage.npm.help.schema.json +0 -6
- package/dist/breakage.npm.schema.json +0 -6
- package/dist/breakage.npm.test.$peer.$range.schema.json +0 -16
- package/dist/breakage.npm.test.schema.json +0 -16
- package/dist/breakage.prelude.$configPath.schema.json +0 -31
- package/dist/breakage.prelude.schema.json +0 -31
- package/dist/breakage.schema.schema.json +0 -10
|
@@ -1,15 +1,125 @@
|
|
|
1
|
+
import { a as validateRelease, n as releaseSchema, o as versionDelta, r as resolveReleaseDelta, t as rejectCertificationOptions } from "./release-BsdIDT4g.js";
|
|
1
2
|
import { createRequire } from "node:module";
|
|
2
|
-
import { exec, spawn } from "node:child_process";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { minimatch } from "minimatch";
|
|
5
5
|
import { simpleGit } from "simple-git";
|
|
6
6
|
import logger from "takua";
|
|
7
|
-
import {
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
8
|
import * as fs from "node:fs/promises";
|
|
9
9
|
import { chmod, copyFile, lstat, mkdir, readFile, readdir, realpath, rename, rm, rmdir, writeFile } from "node:fs/promises";
|
|
10
|
-
import { setTimeout } from "node:timers/promises";
|
|
11
10
|
import semver from "semver";
|
|
12
|
-
|
|
11
|
+
import { type } from "arktype";
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { setTimeout } from "node:timers/promises";
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { cli, options } from "comline";
|
|
16
|
+
// src/task.ts
|
|
17
|
+
/** Capture output per environment so parallel runs cannot interleave their reports. */
|
|
18
|
+
async function command(commandLine, cwd) {
|
|
19
|
+
if (typeof commandLine !== `string` || !commandLine.trim()) throw new Error(`Commands must be nonempty shell strings.`);
|
|
20
|
+
return await new Promise((resolve, reject) => {
|
|
21
|
+
const child = spawn(commandLine, {
|
|
22
|
+
cwd,
|
|
23
|
+
shell: true,
|
|
24
|
+
env: {
|
|
25
|
+
...process.env,
|
|
26
|
+
NODE_PATH: ``,
|
|
27
|
+
PATH: [
|
|
28
|
+
path.join(cwd, `node_modules/.bin`),
|
|
29
|
+
path.join(cwd, `../node_modules/.bin`),
|
|
30
|
+
process.env.PATH
|
|
31
|
+
].join(path.delimiter)
|
|
32
|
+
},
|
|
33
|
+
stdio: [
|
|
34
|
+
`ignore`,
|
|
35
|
+
`pipe`,
|
|
36
|
+
`pipe`
|
|
37
|
+
]
|
|
38
|
+
});
|
|
39
|
+
let output = ``;
|
|
40
|
+
const collect = (chunk) => {
|
|
41
|
+
output = (output + chunk.toString()).slice(-1e6);
|
|
42
|
+
};
|
|
43
|
+
child.stdout.on(`data`, collect);
|
|
44
|
+
child.stderr.on(`data`, collect);
|
|
45
|
+
child.on(`error`, reject);
|
|
46
|
+
child.on(`close`, (code, signal) => resolve({
|
|
47
|
+
code: signal ? 2 : code ?? 2,
|
|
48
|
+
output
|
|
49
|
+
}));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async function prepare(commandLine, cwd) {
|
|
53
|
+
const result = await command(commandLine, cwd);
|
|
54
|
+
if (result.code !== 0) throw new Error(`Command failed (${result.code}) in ${cwd}: ${commandLine}\n${result.output}`);
|
|
55
|
+
}
|
|
56
|
+
function errorMessage(error) {
|
|
57
|
+
return error instanceof Error ? `${error.message}${error.cause ? `\n${errorMessage(error.cause)}` : ``}` : String(error);
|
|
58
|
+
}
|
|
59
|
+
// src/npm/manifest.ts
|
|
60
|
+
function record(value) {
|
|
61
|
+
return typeof value === `object` && value !== null && !Array.isArray(value);
|
|
62
|
+
}
|
|
63
|
+
function packageName(value) {
|
|
64
|
+
return /^(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/.test(value);
|
|
65
|
+
}
|
|
66
|
+
function parseManifest(value, label) {
|
|
67
|
+
if (!record(value) || typeof value.name !== `string` || !packageName(value.name) || typeof value.version !== `string` || !semver.valid(value.version)) throw new Error(`Invalid package identity in ${label}.`);
|
|
68
|
+
if (value.peerDependenciesMeta !== void 0 && (!record(value.peerDependenciesMeta) || Object.values(value.peerDependenciesMeta).some((entry) => !record(entry) || entry.optional !== void 0 && typeof entry.optional !== `boolean`))) throw new Error(`Invalid peerDependenciesMeta in ${label}.`);
|
|
69
|
+
for (const field of [
|
|
70
|
+
`dependencies`,
|
|
71
|
+
`optionalDependencies`,
|
|
72
|
+
`devDependencies`,
|
|
73
|
+
`peerDependencies`
|
|
74
|
+
]) {
|
|
75
|
+
const entries = value[field];
|
|
76
|
+
if (entries !== void 0 && (!record(entries) || Object.entries(entries).some(([name, spec]) => !packageName(name) || typeof spec !== `string`))) throw new Error(`Invalid ${field} in ${label}.`);
|
|
77
|
+
}
|
|
78
|
+
if (value.breakage !== void 0 && (!record(value.breakage) || value.breakage.candidates !== void 0 && (!Array.isArray(value.breakage.candidates) || value.breakage.candidates.some((name) => typeof name !== `string` || !packageName(name))))) throw new Error(`Invalid candidate peers in ${label}.`);
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
async function readManifest(filename) {
|
|
82
|
+
return parseManifest(JSON.parse(await readFile(filename, `utf8`)), filename);
|
|
83
|
+
}
|
|
84
|
+
/** Current support is authored explicitly; only historical shorthand is resolved. */
|
|
85
|
+
async function normalizeWorkspacePeers(manifest, versionAtBaseline) {
|
|
86
|
+
if (!manifest.peerDependencies) return manifest;
|
|
87
|
+
const entries = await Promise.all(Object.entries(manifest.peerDependencies).map(async ([peer, range]) => {
|
|
88
|
+
if (!range.startsWith(`workspace:`)) return [peer, range];
|
|
89
|
+
const requested = range.slice(`workspace:`.length);
|
|
90
|
+
if ([
|
|
91
|
+
``,
|
|
92
|
+
`*`,
|
|
93
|
+
`^`,
|
|
94
|
+
`~`
|
|
95
|
+
].includes(requested)) {
|
|
96
|
+
if (!versionAtBaseline) throw new Error(`Moving workspace peer range ${peer}: ${range} in ${manifest.name}. Declare explicit support eras, for example ^1.0.0 || ^2.0.0, so local version changes cannot change the support promise.`);
|
|
97
|
+
const version = await versionAtBaseline(peer);
|
|
98
|
+
if (!semver.valid(version)) throw new Error(`Invalid historical workspace version for ${peer}: ${version}.`);
|
|
99
|
+
return [peer, `${requested === `^` || requested === `~` ? requested : ``}${version}`];
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
eras(requested);
|
|
103
|
+
} catch {
|
|
104
|
+
throw new Error(`Unsupported workspace peer range for ${peer}: ${range}.`);
|
|
105
|
+
}
|
|
106
|
+
return [peer, requested];
|
|
107
|
+
}));
|
|
108
|
+
return {
|
|
109
|
+
...manifest,
|
|
110
|
+
peerDependencies: Object.fromEntries(entries)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function eras(range) {
|
|
114
|
+
const parts = range.split(`||`).map((part) => part.trim());
|
|
115
|
+
if (parts.some((part) => !part || semver.validRange(part) === null)) throw new Error(`Invalid peer range: ${range}.`);
|
|
116
|
+
return parts;
|
|
117
|
+
}
|
|
118
|
+
/** Previews only represent an explicitly enrolled exact preview. */
|
|
119
|
+
function matches(version, range) {
|
|
120
|
+
return eras(range).some((era) => semver.prerelease(version) ? semver.valid(era.replace(/^=\s*/, ``)) === semver.valid(version) : semver.satisfies(version, era));
|
|
121
|
+
}
|
|
122
|
+
// src/directory-lock.ts
|
|
13
123
|
async function withDirectoryLock(lock, action, waitForCleanup = false) {
|
|
14
124
|
const deadline = Date.now() + 6e4;
|
|
15
125
|
for (;;) try {
|
|
@@ -26,8 +136,7 @@ async function withDirectoryLock(lock, action, waitForCleanup = false) {
|
|
|
26
136
|
await rmdir(lock);
|
|
27
137
|
}
|
|
28
138
|
}
|
|
29
|
-
|
|
30
|
-
//#region src/release-tag.ts
|
|
139
|
+
// src/release-tag.ts
|
|
31
140
|
function compareInteger(a, b) {
|
|
32
141
|
return a.length - b.length || (a > b ? 1 : a < b ? -1 : 0);
|
|
33
142
|
}
|
|
@@ -69,8 +178,7 @@ function latestReleaseTag(remoteTags, tagPattern) {
|
|
|
69
178
|
}
|
|
70
179
|
return latest?.ref;
|
|
71
180
|
}
|
|
72
|
-
|
|
73
|
-
//#region src/baseline.ts
|
|
181
|
+
// src/baseline.ts
|
|
74
182
|
const objectId = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/;
|
|
75
183
|
function isBaseline(value) {
|
|
76
184
|
if (typeof value !== `object` || value === null) return false;
|
|
@@ -149,8 +257,7 @@ async function readBaseline(git, filename, { tagPattern }) {
|
|
|
149
257
|
throw new Error(`Missing, invalid, or unusable baseline ${filename}. Run breakage prelude again.`, { cause });
|
|
150
258
|
}
|
|
151
259
|
}
|
|
152
|
-
|
|
153
|
-
//#region src/test-file-state.ts
|
|
260
|
+
// src/test-file-state.ts
|
|
154
261
|
function isActiveCheck(value) {
|
|
155
262
|
if (typeof value !== `object` || value === null) return false;
|
|
156
263
|
const record = value;
|
|
@@ -293,127 +400,48 @@ async function withTestFileState(git, baseDirname, action) {
|
|
|
293
400
|
});
|
|
294
401
|
});
|
|
295
402
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
child.on(`close`, (code, signal) => resolve({
|
|
327
|
-
code: signal ? 2 : code ?? 2,
|
|
328
|
-
output
|
|
329
|
-
}));
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
async function prepare(commandLine, cwd) {
|
|
333
|
-
const result = await command(commandLine, cwd);
|
|
334
|
-
if (result.code !== 0) throw new Error(`Command failed (${result.code}) in ${cwd}: ${commandLine}\n${result.output}`);
|
|
335
|
-
}
|
|
336
|
-
function errorMessage(error) {
|
|
337
|
-
return error instanceof Error ? `${error.message}${error.cause ? `\n${errorMessage(error.cause)}` : ``}` : String(error);
|
|
338
|
-
}
|
|
339
|
-
//#endregion
|
|
340
|
-
//#region src/npm/manifest.ts
|
|
341
|
-
function record(value) {
|
|
342
|
-
return typeof value === `object` && value !== null && !Array.isArray(value);
|
|
343
|
-
}
|
|
344
|
-
function packageName(value) {
|
|
345
|
-
return /^(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/.test(value);
|
|
346
|
-
}
|
|
347
|
-
function parseManifest(value, label) {
|
|
348
|
-
if (!record(value) || typeof value.name !== `string` || !packageName(value.name) || typeof value.version !== `string` || !semver.valid(value.version)) throw new Error(`Invalid package identity in ${label}.`);
|
|
349
|
-
if (value.peerDependenciesMeta !== void 0 && (!record(value.peerDependenciesMeta) || Object.values(value.peerDependenciesMeta).some((entry) => !record(entry) || entry.optional !== void 0 && typeof entry.optional !== `boolean`))) throw new Error(`Invalid peerDependenciesMeta in ${label}.`);
|
|
350
|
-
for (const field of [
|
|
351
|
-
`dependencies`,
|
|
352
|
-
`optionalDependencies`,
|
|
353
|
-
`devDependencies`,
|
|
354
|
-
`peerDependencies`
|
|
355
|
-
]) {
|
|
356
|
-
const entries = value[field];
|
|
357
|
-
if (entries !== void 0 && (!record(entries) || Object.entries(entries).some(([name, spec]) => !packageName(name) || typeof spec !== `string`))) throw new Error(`Invalid ${field} in ${label}.`);
|
|
358
|
-
}
|
|
359
|
-
if (value.breakage !== void 0 && (!record(value.breakage) || value.breakage.candidates !== void 0 && (!Array.isArray(value.breakage.candidates) || value.breakage.candidates.some((name) => typeof name !== `string` || !packageName(name))))) throw new Error(`Invalid candidate peers in ${label}.`);
|
|
360
|
-
return value;
|
|
361
|
-
}
|
|
362
|
-
async function readManifest(filename) {
|
|
363
|
-
return parseManifest(JSON.parse(await readFile(filename, `utf8`)), filename);
|
|
364
|
-
}
|
|
365
|
-
/** Current support is authored explicitly; only historical shorthand is resolved. */
|
|
366
|
-
async function normalizeWorkspacePeers(manifest, versionAtBaseline) {
|
|
367
|
-
if (!manifest.peerDependencies) return manifest;
|
|
368
|
-
const entries = await Promise.all(Object.entries(manifest.peerDependencies).map(async ([peer, range]) => {
|
|
369
|
-
if (!range.startsWith(`workspace:`)) return [peer, range];
|
|
370
|
-
const requested = range.slice(`workspace:`.length);
|
|
371
|
-
if ([
|
|
372
|
-
``,
|
|
373
|
-
`*`,
|
|
374
|
-
`^`,
|
|
375
|
-
`~`
|
|
376
|
-
].includes(requested)) {
|
|
377
|
-
if (!versionAtBaseline) throw new Error(`Moving workspace peer range ${peer}: ${range} in ${manifest.name}. Declare explicit support eras, for example ^1.0.0 || ^2.0.0, so local version changes cannot change the support promise.`);
|
|
378
|
-
const version = await versionAtBaseline(peer);
|
|
379
|
-
if (!semver.valid(version)) throw new Error(`Invalid historical workspace version for ${peer}: ${version}.`);
|
|
380
|
-
return [peer, `${requested === `^` || requested === `~` ? requested : ``}${version}`];
|
|
381
|
-
}
|
|
382
|
-
try {
|
|
383
|
-
eras(requested);
|
|
384
|
-
} catch {
|
|
385
|
-
throw new Error(`Unsupported workspace peer range for ${peer}: ${range}.`);
|
|
386
|
-
}
|
|
387
|
-
return [peer, requested];
|
|
388
|
-
}));
|
|
389
|
-
return {
|
|
390
|
-
...manifest,
|
|
391
|
-
peerDependencies: Object.fromEntries(entries)
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
function eras(range) {
|
|
395
|
-
const parts = range.split(`||`).map((part) => part.trim());
|
|
396
|
-
if (parts.some((part) => !part || semver.validRange(part) === null)) throw new Error(`Invalid peer range: ${range}.`);
|
|
397
|
-
return parts;
|
|
398
|
-
}
|
|
399
|
-
/** Previews only represent an explicitly enrolled exact preview. */
|
|
400
|
-
function matches(version, range) {
|
|
401
|
-
return eras(range).some((era) => semver.prerelease(version) ? semver.valid(era.replace(/^=\s*/, ``)) === semver.valid(version) : semver.satisfies(version, era));
|
|
402
|
-
}
|
|
403
|
-
//#endregion
|
|
404
|
-
//#region src/npm/config.ts
|
|
403
|
+
// src/npm/config.ts
|
|
404
|
+
const npmSuiteSchema = type({
|
|
405
|
+
"+": `reject`,
|
|
406
|
+
files: `string[]`,
|
|
407
|
+
command: `string`
|
|
408
|
+
});
|
|
409
|
+
const npmConfigSchema = type({
|
|
410
|
+
"+": `reject`,
|
|
411
|
+
"release?": releaseSchema,
|
|
412
|
+
"packageDirectory?": `string`,
|
|
413
|
+
artifact: `string`,
|
|
414
|
+
representatives: `string | string[]`,
|
|
415
|
+
lockfile: `string`,
|
|
416
|
+
prepareCommand: `string`,
|
|
417
|
+
"installCommand?": `string`,
|
|
418
|
+
suites: { "[string]": npmSuiteSchema },
|
|
419
|
+
"localPeers?": { "[string]": {
|
|
420
|
+
"+": `reject`,
|
|
421
|
+
directory: `string`,
|
|
422
|
+
artifact: `string`
|
|
423
|
+
} },
|
|
424
|
+
baseline: type({
|
|
425
|
+
"+": `reject`,
|
|
426
|
+
firstRelease: `true`
|
|
427
|
+
}).or({
|
|
428
|
+
"+": `reject`,
|
|
429
|
+
"tagPattern?": `string`,
|
|
430
|
+
"baselineFile?": `string`
|
|
431
|
+
})
|
|
432
|
+
});
|
|
405
433
|
function nonempty(value) {
|
|
406
434
|
return typeof value === `string` && value.trim().length > 0;
|
|
407
435
|
}
|
|
408
436
|
function validateConfig(value) {
|
|
409
437
|
if (!record(value)) throw new Error(`Configuration must have an npm object.`);
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
`prepareCommand`
|
|
414
|
-
]) if (!nonempty(value[key])) throw new Error(`npm.${key} must be a nonempty string.`);
|
|
438
|
+
rejectCertificationOptions(value);
|
|
439
|
+
npmConfigSchema.assert(value);
|
|
440
|
+
for (const key of [`artifact`, `lockfile`]) if (!nonempty(value[key])) throw new Error(`npm.${key} must be a nonempty string.`);
|
|
415
441
|
if (path.isAbsolute(value.lockfile) || value.lockfile.split(/[\\/]/).includes(`..`)) throw new Error(`npm.lockfile must stay inside each representative.`);
|
|
416
|
-
|
|
442
|
+
if (!nonempty(value.prepareCommand)) throw new Error(`npm.prepareCommand must be a nonempty shell command string.`);
|
|
443
|
+
if (value.installCommand !== void 0 && !nonempty(value.installCommand)) throw new Error(`Invalid npm.installCommand.`);
|
|
444
|
+
for (const key of [`packageDirectory`]) if (value[key] !== void 0 && !nonempty(value[key])) throw new Error(`Invalid npm.${key}.`);
|
|
417
445
|
const globs = typeof value.representatives === `string` ? [value.representatives] : value.representatives;
|
|
418
446
|
if (!Array.isArray(globs) || !globs.length || globs.some((glob) => !nonempty(glob))) throw new Error(`npm.representatives must contain directory globs.`);
|
|
419
447
|
if (!record(value.suites) || !Object.keys(value.suites).length) throw new Error(`Configure at least one npm peer suite.`);
|
|
@@ -423,23 +451,96 @@ function validateConfig(value) {
|
|
|
423
451
|
if (!record(value.localPeers)) throw new Error(`Invalid npm.localPeers.`);
|
|
424
452
|
for (const [peer, local] of Object.entries(value.localPeers)) if (!packageName(peer) || !record(local) || !nonempty(local.directory) || !nonempty(local.artifact)) throw new Error(`Invalid local peer ${peer}.`);
|
|
425
453
|
}
|
|
426
|
-
for (const hook of [`releaseIntent`, `certifyBreaking`]) if (value[hook] !== void 0 && typeof value[hook] !== `function`) throw new Error(`npm.${hook} must be a function.`);
|
|
427
454
|
}
|
|
428
455
|
function validatePeerRanges(peers) {
|
|
429
456
|
for (const range of Object.values(peers)) eras(range);
|
|
430
457
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
458
|
+
// src/config.ts
|
|
459
|
+
const configFields = {
|
|
460
|
+
tagPattern: type(`string`),
|
|
461
|
+
baselineFile: type(`string`),
|
|
462
|
+
testPattern: type(`string`),
|
|
463
|
+
testCommand: type(`string`),
|
|
464
|
+
test: type(`string`),
|
|
465
|
+
baseDirname: type(`string`),
|
|
466
|
+
verbose: type(`boolean`),
|
|
467
|
+
out: type(`string`)
|
|
468
|
+
};
|
|
469
|
+
const configSchema = type({
|
|
470
|
+
"+": `reject`,
|
|
471
|
+
"tagPattern?": configFields.tagPattern.or(`undefined`),
|
|
472
|
+
"baselineFile?": configFields.baselineFile.or(`undefined`),
|
|
473
|
+
"testPattern?": configFields.testPattern,
|
|
474
|
+
"testCommand?": configFields.testCommand,
|
|
475
|
+
"test?": configFields.test,
|
|
476
|
+
"baseDirname?": configFields.baseDirname,
|
|
477
|
+
"verbose?": configFields.verbose.or(`undefined`),
|
|
478
|
+
"out?": configFields.out,
|
|
479
|
+
"release?": releaseSchema,
|
|
480
|
+
"npm?": npmConfigSchema
|
|
481
|
+
});
|
|
482
|
+
/** The complete config, with no fields exposed to argv by this loader. */
|
|
483
|
+
const configOptions = options(`Breakage configuration`, configSchema, {
|
|
484
|
+
tagPattern: { configOnly: true },
|
|
485
|
+
baselineFile: { configOnly: true },
|
|
486
|
+
testPattern: { configOnly: true },
|
|
487
|
+
testCommand: { configOnly: true },
|
|
488
|
+
test: { configOnly: true },
|
|
489
|
+
baseDirname: { configOnly: true },
|
|
490
|
+
verbose: { configOnly: true },
|
|
491
|
+
out: { configOnly: true },
|
|
492
|
+
release: { configOnly: true },
|
|
493
|
+
npm: { configOnly: true }
|
|
494
|
+
});
|
|
495
|
+
/** Share the root release model with npm checks unless they supply their own. */
|
|
496
|
+
function defineConfig(config) {
|
|
497
|
+
if (config && typeof config === `object`) {
|
|
498
|
+
rejectCertificationOptions(config);
|
|
499
|
+
if (config.npm) rejectCertificationOptions(config.npm);
|
|
500
|
+
if (config.release !== void 0) validateRelease(config.release);
|
|
501
|
+
if (config.npm?.release !== void 0) validateRelease(config.npm.release);
|
|
502
|
+
}
|
|
503
|
+
configSchema.assert(config);
|
|
438
504
|
return {
|
|
439
|
-
|
|
440
|
-
|
|
505
|
+
...config,
|
|
506
|
+
...config.npm ? { npm: {
|
|
507
|
+
...config.npm,
|
|
508
|
+
...config.release && config.npm.release === void 0 ? { release: config.release } : {}
|
|
509
|
+
} } : {}
|
|
441
510
|
};
|
|
442
511
|
}
|
|
512
|
+
function defaultConfig(directory) {
|
|
513
|
+
const filename = path.join(directory, `breakage.config.ts`);
|
|
514
|
+
if (!existsSync(filename) && existsSync(path.join(directory, `breakage.config.json`))) throw new Error(`JSON configuration was removed. Migrate breakage.config.json to breakage.config.ts.`);
|
|
515
|
+
return filename;
|
|
516
|
+
}
|
|
517
|
+
/** Comline ignores absent discovered files; explicit paths must exist. */
|
|
518
|
+
function configPath(filename, required = true) {
|
|
519
|
+
const absolute = path.resolve(filename);
|
|
520
|
+
if (path.extname(absolute) !== `.ts`) throw new Error(`Configuration must be a .ts module with a default export; JSON configuration was removed.`);
|
|
521
|
+
if (required && !existsSync(absolute)) throw new Error(`Configuration file does not exist: ${absolute}`);
|
|
522
|
+
return absolute;
|
|
523
|
+
}
|
|
524
|
+
/** Apply application semantics after Comline validates and merges configuration. */
|
|
525
|
+
function resolveConfig(config, filename) {
|
|
526
|
+
return {
|
|
527
|
+
...defineConfig(config),
|
|
528
|
+
baseDirname: path.resolve(filename ? path.dirname(filename) : process.cwd(), config.baseDirname ?? `.`)
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
/** Use Comline's module loading and full-schema validation for programmatic and npm consumers. */
|
|
532
|
+
async function loadConfig(filename) {
|
|
533
|
+
const absolute = configPath(filename);
|
|
534
|
+
return resolveConfig((await cli({
|
|
535
|
+
cliName: `breakage`,
|
|
536
|
+
routeOptions: { "": configOptions },
|
|
537
|
+
discoverConfigPath: () => absolute
|
|
538
|
+
}).async([process.execPath, `breakage`])).inputs.opts, absolute);
|
|
539
|
+
}
|
|
540
|
+
// src/npm/release.ts
|
|
541
|
+
async function releaseVersion(config, context) {
|
|
542
|
+
return config.release ? (await resolveReleaseDelta(config.release, context))?.version : void 0;
|
|
543
|
+
}
|
|
443
544
|
/**
|
|
444
545
|
* Compare the union, including adjacent alternatives. semver.subset compares each
|
|
445
546
|
* conjunction to a single conjunction and cannot prove coverage by their union.
|
|
@@ -518,8 +619,7 @@ async function previousManifest(config, directory) {
|
|
|
518
619
|
return historical.version;
|
|
519
620
|
});
|
|
520
621
|
}
|
|
521
|
-
|
|
522
|
-
//#region src/npm/environment.ts
|
|
622
|
+
// src/npm/environment.ts
|
|
523
623
|
function within(parent, child) {
|
|
524
624
|
const relative = path.relative(parent, child);
|
|
525
625
|
return relative === `` || !relative.startsWith(`..${path.sep}`) && relative !== `..` && !path.isAbsolute(relative);
|
|
@@ -624,13 +724,13 @@ async function maySelectEnvironment(config, base, directory, packageManifest, se
|
|
|
624
724
|
const packageDirectory = path.resolve(base, local.directory);
|
|
625
725
|
const current = await readManifest(path.join(packageDirectory, `package.json`));
|
|
626
726
|
if (current.name !== peer) throw new Error(`Local peer identity mismatch for ${peer}.`);
|
|
627
|
-
const release = await
|
|
727
|
+
const release = await releaseVersion(config, {
|
|
628
728
|
packageName: peer,
|
|
629
729
|
packageDirectory,
|
|
630
730
|
currentVersion: current.version
|
|
631
731
|
});
|
|
632
|
-
if (release
|
|
633
|
-
return matches(release
|
|
732
|
+
if (release === void 0) throw new Error(`Candidate ${peer} has no release version.`);
|
|
733
|
+
return matches(release, range);
|
|
634
734
|
}
|
|
635
735
|
const declared = declarations[peer];
|
|
636
736
|
return declared !== void 0 && matches(declared, range);
|
|
@@ -652,17 +752,17 @@ async function prepareEnvironment(config, base, directory, stage, packageManifes
|
|
|
652
752
|
const localManifest = await normalizeWorkspacePeers(await readManifest(path.join(localDirectory, `package.json`)));
|
|
653
753
|
if (localManifest.name !== peer) throw new Error(`Local peer identity mismatch for ${peer}.`);
|
|
654
754
|
validatePeerRanges(localManifest.peerDependencies ?? {});
|
|
655
|
-
const evidence = await
|
|
755
|
+
const evidence = await releaseVersion(config, {
|
|
656
756
|
packageName: peer,
|
|
657
757
|
packageDirectory: localDirectory,
|
|
658
758
|
currentVersion: localManifest.version
|
|
659
759
|
});
|
|
660
|
-
if (evidence
|
|
760
|
+
if (evidence === void 0) throw new Error(`Candidate ${peer} has no release version.`);
|
|
661
761
|
candidates[peer] = {
|
|
662
762
|
directory: localDirectory,
|
|
663
763
|
artifact: path.resolve(base, local.artifact),
|
|
664
764
|
currentVersion: localManifest.version,
|
|
665
|
-
version: evidence
|
|
765
|
+
version: evidence,
|
|
666
766
|
manifest: localManifest
|
|
667
767
|
};
|
|
668
768
|
}
|
|
@@ -866,8 +966,7 @@ async function verifyResolution(manifest, environment) {
|
|
|
866
966
|
} else pending.push(await resolvePackage(directory, dependency, installation));
|
|
867
967
|
}
|
|
868
968
|
}
|
|
869
|
-
|
|
870
|
-
//#region src/npm/check.ts
|
|
969
|
+
// src/npm/check.ts
|
|
871
970
|
async function checkSupport(config, base, manifest) {
|
|
872
971
|
try {
|
|
873
972
|
const previous = await previousManifest(config, base);
|
|
@@ -882,29 +981,20 @@ async function checkSupport(config, base, manifest) {
|
|
|
882
981
|
previousVersion: previous.version,
|
|
883
982
|
changes
|
|
884
983
|
};
|
|
885
|
-
const evidence = await
|
|
984
|
+
const evidence = config.release ? await resolveReleaseDelta(config.release, {
|
|
886
985
|
packageName: manifest.name,
|
|
887
986
|
packageDirectory: base,
|
|
888
|
-
currentVersion: manifest.version
|
|
889
|
-
|
|
890
|
-
|
|
987
|
+
currentVersion: manifest.version,
|
|
988
|
+
baseline: { version: previous.version }
|
|
989
|
+
}) : void 0;
|
|
990
|
+
if (evidence === void 0) return {
|
|
891
991
|
status: `end-of-support`,
|
|
892
992
|
previousVersion: previous.version,
|
|
893
993
|
changes
|
|
894
994
|
};
|
|
895
|
-
|
|
896
|
-
const release = {
|
|
897
|
-
version: evidence.version,
|
|
898
|
-
delta: semver.diff(previous.version, evidence.version) ?? `none`
|
|
899
|
-
};
|
|
900
|
-
const certified = config.certifyBreaking ? await config.certifyBreaking({
|
|
901
|
-
previousVersion: previous.version,
|
|
902
|
-
release,
|
|
903
|
-
changes
|
|
904
|
-
}) : false;
|
|
905
|
-
if (typeof certified !== `boolean`) throw new Error(`certifyBreaking must return a boolean.`);
|
|
995
|
+
const release = versionDelta(evidence.previousVersion, evidence.version, config.release?.policy);
|
|
906
996
|
return {
|
|
907
|
-
status:
|
|
997
|
+
status: release.breaking ? `breaking-release` : `end-of-support`,
|
|
908
998
|
previousVersion: previous.version,
|
|
909
999
|
changes,
|
|
910
1000
|
release
|
|
@@ -925,14 +1015,14 @@ async function checkLocalReleases(config, base, manifest, representatives) {
|
|
|
925
1015
|
const directory = path.resolve(base, local.directory);
|
|
926
1016
|
const current = await readManifest(path.join(directory, `package.json`));
|
|
927
1017
|
if (current.name !== peer) throw new Error(`Local peer identity mismatch for ${peer}.`);
|
|
928
|
-
const release = await
|
|
1018
|
+
const release = await releaseVersion(config, {
|
|
929
1019
|
packageName: peer,
|
|
930
1020
|
packageDirectory: directory,
|
|
931
1021
|
currentVersion: current.version
|
|
932
1022
|
});
|
|
933
|
-
if (release
|
|
934
|
-
if (!matches(release
|
|
935
|
-
if (!representatives.some((result) => result.peers[peer]?.source === `candidate` && result.peers[peer]?.version === release
|
|
1023
|
+
if (release === void 0) return [];
|
|
1024
|
+
if (!matches(release, range)) return [`Declared support for ${peer}: ${range} does not include its planned local release ${release}. Update the peer range to include the intended version.`];
|
|
1025
|
+
if (!representatives.some((result) => result.peers[peer]?.source === `candidate` && result.peers[peer]?.version === release && result.suites.some((suite) => suite.peer === peer))) return [`No coordinated representative covers planned local peer ${peer}@${release}. Enroll ${peer} with breakage.candidates and configure its relationship suite.`];
|
|
936
1026
|
return [];
|
|
937
1027
|
} catch (error) {
|
|
938
1028
|
return [`Cannot inspect planned local release for ${peer}: ${errorMessage(error)}`];
|
|
@@ -1028,17 +1118,25 @@ async function run(config, baseDirectory, coverageOnly, selection) {
|
|
|
1028
1118
|
eras(selection.range);
|
|
1029
1119
|
}
|
|
1030
1120
|
const releases = /* @__PURE__ */ new Map();
|
|
1031
|
-
const
|
|
1121
|
+
const resolveDelta = (context) => {
|
|
1122
|
+
const key = JSON.stringify([
|
|
1123
|
+
context.packageDirectory,
|
|
1124
|
+
context.currentVersion,
|
|
1125
|
+
context.baseline
|
|
1126
|
+
]);
|
|
1127
|
+
let pending = releases.get(key);
|
|
1128
|
+
if (!pending) {
|
|
1129
|
+
pending = resolveReleaseDelta(config.release, context);
|
|
1130
|
+
releases.set(key, pending);
|
|
1131
|
+
}
|
|
1132
|
+
return pending;
|
|
1133
|
+
};
|
|
1032
1134
|
const effective = {
|
|
1033
1135
|
...config,
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
releases.set(context.packageDirectory, pending);
|
|
1039
|
-
}
|
|
1040
|
-
return pending;
|
|
1041
|
-
}
|
|
1136
|
+
...config.release ? { release: {
|
|
1137
|
+
...config.release,
|
|
1138
|
+
resolveDelta
|
|
1139
|
+
} } : {}
|
|
1042
1140
|
};
|
|
1043
1141
|
const directories = await discover(config, base);
|
|
1044
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)))]);
|
|
@@ -1090,9 +1188,14 @@ function formatNpmReport(report) {
|
|
|
1090
1188
|
if (report.mode === `coverage-only`) lines.push(`Coverage audit only; compatibility assertions were not executed.`);
|
|
1091
1189
|
return `${lines.join(`\n`)}\n`;
|
|
1092
1190
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1191
|
+
// src/breakage.ts
|
|
1192
|
+
async function breakage(options) {
|
|
1193
|
+
rejectCertificationOptions(options);
|
|
1194
|
+
const { tagPattern, baselineFile, testPattern, testCommand, test, release, baseDirname = process.cwd(), verbose = false } = options;
|
|
1195
|
+
if (!testPattern || !test && !testCommand) throw new Error(`required arguments were not provided: testPattern and test or testCommand.`);
|
|
1196
|
+
if (test !== void 0 && testCommand !== void 0) throw new Error(`Choose test or testCommand, not both.`);
|
|
1197
|
+
if (typeof (test ?? testCommand) !== `string` || !(test ?? testCommand).trim()) throw new Error(`test must be a nonempty shell command string.`);
|
|
1198
|
+
if (release !== void 0) validateRelease(release);
|
|
1096
1199
|
let mark;
|
|
1097
1200
|
let logMarks;
|
|
1098
1201
|
if (verbose) {
|
|
@@ -1163,6 +1266,24 @@ async function breakage({ tagPattern, baselineFile, testPattern, testCommand, ce
|
|
|
1163
1266
|
mark?.(`listed all files checked into git`);
|
|
1164
1267
|
const productionTestFiles = productionFiles.filter((file) => minimatch(file, testPattern));
|
|
1165
1268
|
mark?.(`filtered to public test files`);
|
|
1269
|
+
let releaseEvidence;
|
|
1270
|
+
if (release) {
|
|
1271
|
+
const current = await readManifest(path.join(baseDirname, `package.json`));
|
|
1272
|
+
const prefix = await git.revparse([`--show-prefix`]);
|
|
1273
|
+
const previous = parseManifest(JSON.parse(await git.show([`${releaseCommit}:${prefix}package.json`])), `baseline`);
|
|
1274
|
+
if (previous.name !== current.name) throw new Error(`Release baseline belongs to another package.`);
|
|
1275
|
+
const transition = await resolveReleaseDelta(release, {
|
|
1276
|
+
packageName: current.name,
|
|
1277
|
+
packageDirectory: path.resolve(baseDirname),
|
|
1278
|
+
currentVersion: current.version,
|
|
1279
|
+
baseline: {
|
|
1280
|
+
version: previous.version,
|
|
1281
|
+
ref: latestReleaseTag$1,
|
|
1282
|
+
commit: releaseCommit
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
if (transition !== void 0) releaseEvidence = versionDelta(transition.previousVersion, transition.version, release.policy);
|
|
1286
|
+
}
|
|
1166
1287
|
const prepared = await withTestFileState(git, baseDirname, async (state) => {
|
|
1167
1288
|
if (!await state.isClean()) return {
|
|
1168
1289
|
summary: `The git repository must be clean to run this command.`,
|
|
@@ -1186,50 +1307,43 @@ async function breakage({ tagPattern, baselineFile, testPattern, testCommand, ce
|
|
|
1186
1307
|
if (prepared.summary !== void 0) return prepared;
|
|
1187
1308
|
const { restore } = prepared;
|
|
1188
1309
|
try {
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
breakingChangesCertified: wereCertified,
|
|
1221
|
-
certificationStdout: stdout,
|
|
1222
|
-
certificationStderr: stderr
|
|
1223
|
-
});
|
|
1224
|
-
});
|
|
1225
|
-
});
|
|
1310
|
+
const result = await command(test ?? testCommand, baseDirname);
|
|
1311
|
+
if (result.code > 1) return {
|
|
1312
|
+
gitWasClean: true,
|
|
1313
|
+
setupError: result.output,
|
|
1314
|
+
summary: `Public test setup failed (${result.code}).`
|
|
1315
|
+
};
|
|
1316
|
+
const common = {
|
|
1317
|
+
gitWasClean: true,
|
|
1318
|
+
lastReleaseFound: true,
|
|
1319
|
+
lastReleaseTag: latestReleaseTag$1,
|
|
1320
|
+
testsWereFound: true,
|
|
1321
|
+
testsFound: productionTestFiles,
|
|
1322
|
+
...releaseEvidence ? { release: releaseEvidence } : {}
|
|
1323
|
+
};
|
|
1324
|
+
if (result.code === 0) return {
|
|
1325
|
+
...common,
|
|
1326
|
+
breakingChangesFound: false,
|
|
1327
|
+
summary: `No breaking changes were detected.`
|
|
1328
|
+
};
|
|
1329
|
+
return {
|
|
1330
|
+
...common,
|
|
1331
|
+
breakingChangesFound: true,
|
|
1332
|
+
testResult: result.output,
|
|
1333
|
+
summary: releaseEvidence?.breaking ? `Breaking changes are covered by release ${releaseEvidence.previousVersion} → ${releaseEvidence.version}.` : `Breaking changes require a breaking release.`
|
|
1334
|
+
};
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
return {
|
|
1337
|
+
gitWasClean: true,
|
|
1338
|
+
setupError: errorMessage(error),
|
|
1339
|
+
summary: `Public test setup failed: ${errorMessage(error)}`
|
|
1340
|
+
};
|
|
1226
1341
|
} finally {
|
|
1227
1342
|
await restore();
|
|
1228
1343
|
mark?.(`restored original public tests`);
|
|
1229
1344
|
logMarks?.();
|
|
1230
1345
|
}
|
|
1231
1346
|
}
|
|
1232
|
-
|
|
1233
|
-
export { validateConfig as a, breakagePrelude as c, npmTest as i, formatNpmReport as n, record as o, npmCheck as r, errorMessage as s, breakage as t };
|
|
1347
|
+
export { configFields as a, configSchema as c, loadConfig as d, resolveConfig as f, errorMessage as h, npmTest as i, defaultConfig as l, breakagePrelude as m, formatNpmReport as n, configOptions as o, validateConfig as p, npmCheck as r, configPath as s, breakage as t, defineConfig as u };
|
|
1234
1348
|
|
|
1235
|
-
//# sourceMappingURL=breakage-
|
|
1349
|
+
//# sourceMappingURL=breakage-DIY49fzP.js.map
|