breakage 0.8.5 → 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 +163 -47
- package/dist/{breakage-B7dJsgcu.js → breakage-DJiIiLzL.js} +958 -221
- package/dist/breakage-DJiIiLzL.js.map +1 -0
- package/dist/breakage.d.ts +239 -71
- package/dist/breakage.d.ts.map +1 -1
- package/dist/breakage.js +3 -1
- package/dist/breakage.x.js +216 -79
- 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 +198 -44
- package/package.json +45 -6
- package/src/breakage.ts +142 -73
- package/src/breakage.x.ts +174 -127
- package/src/changesets.ts +51 -0
- package/src/config.ts +131 -0
- package/src/npm/check.ts +70 -50
- package/src/npm/cli.ts +30 -22
- package/src/npm/config.ts +38 -6
- package/src/npm/environment.ts +10 -10
- package/src/npm/matrix-cli.ts +170 -0
- package/src/npm/matrix-model.ts +395 -0
- package/src/npm/matrix.ts +657 -0
- 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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { assembleReleasePlan } from "@changesets/assemble-release-plan"
|
|
2
|
+
import { readConfig } from "@changesets/config"
|
|
3
|
+
import { readPreState } from "@changesets/pre"
|
|
4
|
+
import { readChangesets } from "@changesets/read"
|
|
5
|
+
import { getPackages } from "@manypkg/get-packages"
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
versionTransition,
|
|
9
|
+
type ReleaseDeltaContext,
|
|
10
|
+
type VersionTransition,
|
|
11
|
+
} from "./release.ts"
|
|
12
|
+
|
|
13
|
+
/** Read the actual Changesets plan, including dependent bumps and prerelease state. */
|
|
14
|
+
export async function readChangesetsReleasePlan(
|
|
15
|
+
directory: string = process.cwd(),
|
|
16
|
+
): Promise<ReadonlyMap<string, string>> {
|
|
17
|
+
const packages = await getPackages(directory)
|
|
18
|
+
const root = packages.rootDir
|
|
19
|
+
const { config, errors } = await readConfig(root, packages)
|
|
20
|
+
if (errors) throw new Error(errors.join(`\n`))
|
|
21
|
+
const [changesets, preState] = await Promise.all([
|
|
22
|
+
readChangesets(root),
|
|
23
|
+
readPreState(root),
|
|
24
|
+
])
|
|
25
|
+
const plan = assembleReleasePlan(changesets, packages, config, preState)
|
|
26
|
+
return new Map(
|
|
27
|
+
plan.releases.flatMap(({ name, newVersion }) =>
|
|
28
|
+
newVersion === undefined ? [] : [[name, newVersion] as const],
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Compare the selected baseline (or current manifest) with the planned or versioned release. */
|
|
34
|
+
export function changesetsReleaseDelta({
|
|
35
|
+
directory,
|
|
36
|
+
}: { directory?: string } = {}): (
|
|
37
|
+
context: ReleaseDeltaContext,
|
|
38
|
+
) => Promise<VersionTransition> {
|
|
39
|
+
return async ({
|
|
40
|
+
packageName,
|
|
41
|
+
packageDirectory,
|
|
42
|
+
currentVersion,
|
|
43
|
+
baseline,
|
|
44
|
+
}) => {
|
|
45
|
+
const version =
|
|
46
|
+
(await readChangesetsReleasePlan(directory ?? packageDirectory)).get(
|
|
47
|
+
packageName,
|
|
48
|
+
) ?? currentVersion
|
|
49
|
+
return versionTransition(baseline?.version ?? currentVersion, version)
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
import { type } from "arktype"
|
|
5
|
+
import { cli, options, type ConfigInput } from "comline"
|
|
6
|
+
|
|
7
|
+
import { npmConfigSchema } from "./npm/config.ts"
|
|
8
|
+
import { npmMatrixConfigSchema } from "./npm/matrix-model.ts"
|
|
9
|
+
import {
|
|
10
|
+
releaseSchema,
|
|
11
|
+
validateRelease,
|
|
12
|
+
rejectCertificationOptions,
|
|
13
|
+
} from "./release.ts"
|
|
14
|
+
|
|
15
|
+
// Reuse these field schemas for CLI metadata even though hooks cannot become JSON Schema.
|
|
16
|
+
export const configFields = {
|
|
17
|
+
tagPattern: type(`string`),
|
|
18
|
+
baselineFile: type(`string`),
|
|
19
|
+
testPattern: type(`string`),
|
|
20
|
+
testCommand: type(`string`),
|
|
21
|
+
test: type(`string`),
|
|
22
|
+
baseDirname: type(`string`),
|
|
23
|
+
verbose: type(`boolean`),
|
|
24
|
+
out: type(`string`),
|
|
25
|
+
}
|
|
26
|
+
export const configSchema = type({
|
|
27
|
+
"+": `reject`,
|
|
28
|
+
"tagPattern?": configFields.tagPattern.or(`undefined`),
|
|
29
|
+
"baselineFile?": configFields.baselineFile.or(`undefined`),
|
|
30
|
+
"testPattern?": configFields.testPattern,
|
|
31
|
+
"testCommand?": configFields.testCommand,
|
|
32
|
+
"test?": configFields.test,
|
|
33
|
+
"baseDirname?": configFields.baseDirname,
|
|
34
|
+
"verbose?": configFields.verbose.or(`undefined`),
|
|
35
|
+
"out?": configFields.out,
|
|
36
|
+
"release?": releaseSchema,
|
|
37
|
+
"npm?": npmConfigSchema,
|
|
38
|
+
"npmMatrix?": npmMatrixConfigSchema,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
/** The complete config, with no fields exposed to argv by this loader. */
|
|
42
|
+
export const configOptions = options(`Breakage configuration`, configSchema, {
|
|
43
|
+
tagPattern: { configOnly: true },
|
|
44
|
+
baselineFile: { configOnly: true },
|
|
45
|
+
testPattern: { configOnly: true },
|
|
46
|
+
testCommand: { configOnly: true },
|
|
47
|
+
test: { configOnly: true },
|
|
48
|
+
baseDirname: { configOnly: true },
|
|
49
|
+
verbose: { configOnly: true },
|
|
50
|
+
out: { configOnly: true },
|
|
51
|
+
release: { configOnly: true },
|
|
52
|
+
npm: { configOnly: true },
|
|
53
|
+
npmMatrix: { configOnly: true },
|
|
54
|
+
})
|
|
55
|
+
export type BreakageConfig = ConfigInput<typeof configOptions>
|
|
56
|
+
|
|
57
|
+
/** Share the root release model with npm checks unless they supply their own. */
|
|
58
|
+
export function defineConfig(config: BreakageConfig): BreakageConfig {
|
|
59
|
+
if (config && typeof config === `object`) {
|
|
60
|
+
rejectCertificationOptions(config)
|
|
61
|
+
if (config.npm) rejectCertificationOptions(config.npm)
|
|
62
|
+
if (config.release !== undefined) validateRelease(config.release)
|
|
63
|
+
if (config.npm?.release !== undefined) validateRelease(config.npm.release)
|
|
64
|
+
}
|
|
65
|
+
configSchema.assert(config)
|
|
66
|
+
return {
|
|
67
|
+
...config,
|
|
68
|
+
...(config.npm
|
|
69
|
+
? {
|
|
70
|
+
npm: {
|
|
71
|
+
...config.npm,
|
|
72
|
+
...(config.release && config.npm.release === undefined
|
|
73
|
+
? { release: config.release }
|
|
74
|
+
: {}),
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
: {}),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function defaultConfig(directory: string): string {
|
|
82
|
+
const filename = path.join(directory, `breakage.config.ts`)
|
|
83
|
+
if (
|
|
84
|
+
!existsSync(filename) &&
|
|
85
|
+
existsSync(path.join(directory, `breakage.config.json`))
|
|
86
|
+
)
|
|
87
|
+
throw new Error(
|
|
88
|
+
`JSON configuration was removed. Migrate breakage.config.json to breakage.config.ts.`,
|
|
89
|
+
)
|
|
90
|
+
return filename
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Comline ignores absent discovered files; explicit paths must exist. */
|
|
94
|
+
export function configPath(filename: string, required = true): string {
|
|
95
|
+
const absolute = path.resolve(filename)
|
|
96
|
+
if (path.extname(absolute) !== `.ts`)
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Configuration must be a .ts module with a default export; JSON configuration was removed.`,
|
|
99
|
+
)
|
|
100
|
+
if (required && !existsSync(absolute))
|
|
101
|
+
throw new Error(`Configuration file does not exist: ${absolute}`)
|
|
102
|
+
return absolute
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Apply application semantics after Comline validates and merges configuration. */
|
|
106
|
+
export function resolveConfig(
|
|
107
|
+
config: BreakageConfig,
|
|
108
|
+
filename?: string,
|
|
109
|
+
): BreakageConfig {
|
|
110
|
+
return {
|
|
111
|
+
...defineConfig(config),
|
|
112
|
+
baseDirname: path.resolve(
|
|
113
|
+
filename ? path.dirname(filename) : process.cwd(),
|
|
114
|
+
config.baseDirname ?? `.`,
|
|
115
|
+
),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Use Comline's module loading and full-schema validation for programmatic and npm consumers. */
|
|
120
|
+
export async function loadConfig(filename: string): Promise<BreakageConfig> {
|
|
121
|
+
const absolute = configPath(filename)
|
|
122
|
+
const parse = cli({
|
|
123
|
+
cliName: `breakage`,
|
|
124
|
+
routeOptions: { "": configOptions },
|
|
125
|
+
discoverConfigPath: () => absolute,
|
|
126
|
+
})
|
|
127
|
+
return resolveConfig(
|
|
128
|
+
(await parse.async([process.execPath, `breakage`])).inputs.opts,
|
|
129
|
+
absolute,
|
|
130
|
+
)
|
|
131
|
+
}
|
package/src/npm/check.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import {
|
|
4
|
+
resolveReleaseDelta,
|
|
5
|
+
versionDelta,
|
|
6
|
+
type ReleaseDeltaContext,
|
|
7
|
+
type VersionTransition,
|
|
8
|
+
} from "../release.ts"
|
|
9
|
+
import { command, errorMessage } from "../task.ts"
|
|
6
10
|
import { validateConfig, validatePeerRanges } from "./config.ts"
|
|
7
11
|
import {
|
|
8
12
|
checkCandidatePeers,
|
|
@@ -20,13 +24,12 @@ import {
|
|
|
20
24
|
normalizeWorkspacePeers,
|
|
21
25
|
readManifest,
|
|
22
26
|
} from "./manifest.ts"
|
|
23
|
-
import {
|
|
27
|
+
import { releaseVersion, previousManifest, supportChanges } from "./release.ts"
|
|
24
28
|
import type {
|
|
25
29
|
NpmConfig,
|
|
26
30
|
NpmReport,
|
|
27
31
|
NpmRepresentativeResult,
|
|
28
32
|
NpmTestSelection,
|
|
29
|
-
ReleaseIntent,
|
|
30
33
|
} from "./types.ts"
|
|
31
34
|
|
|
32
35
|
async function checkSupport(
|
|
@@ -44,36 +47,28 @@ async function checkSupport(
|
|
|
44
47
|
const changes = supportChanges(previous, manifest)
|
|
45
48
|
if (!changes.length)
|
|
46
49
|
return { status: `unchanged`, previousVersion: previous.version, changes }
|
|
47
|
-
const evidence =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
const evidence = config.release
|
|
51
|
+
? await resolveReleaseDelta(config.release, {
|
|
52
|
+
packageName: manifest.name,
|
|
53
|
+
packageDirectory: base,
|
|
54
|
+
currentVersion: manifest.version,
|
|
55
|
+
baseline: { version: previous.version },
|
|
56
|
+
})
|
|
57
|
+
: undefined
|
|
58
|
+
if (evidence === undefined)
|
|
53
59
|
return {
|
|
54
60
|
status: `end-of-support`,
|
|
55
61
|
previousVersion: previous.version,
|
|
56
62
|
changes,
|
|
57
63
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
delta: semver.diff(previous.version, evidence.version) ?? `none`,
|
|
65
|
-
}
|
|
66
|
-
const certified = config.certifyBreaking
|
|
67
|
-
? await config.certifyBreaking({
|
|
68
|
-
previousVersion: previous.version,
|
|
69
|
-
release,
|
|
70
|
-
changes,
|
|
71
|
-
})
|
|
72
|
-
: false
|
|
73
|
-
if (typeof certified !== `boolean`)
|
|
74
|
-
throw new Error(`certifyBreaking must return a boolean.`)
|
|
64
|
+
const release = versionDelta(
|
|
65
|
+
evidence.previousVersion,
|
|
66
|
+
evidence.version,
|
|
67
|
+
config.release?.policy,
|
|
68
|
+
)
|
|
69
|
+
|
|
75
70
|
return {
|
|
76
|
-
status:
|
|
71
|
+
status: release.breaking ? `breaking-release` : `end-of-support`,
|
|
77
72
|
previousVersion: previous.version,
|
|
78
73
|
changes,
|
|
79
74
|
release,
|
|
@@ -99,26 +94,26 @@ async function checkLocalReleases(
|
|
|
99
94
|
const current = await readManifest(path.join(directory, `package.json`))
|
|
100
95
|
if (current.name !== peer)
|
|
101
96
|
throw new Error(`Local peer identity mismatch for ${peer}.`)
|
|
102
|
-
const release = await
|
|
97
|
+
const release = await releaseVersion(config, {
|
|
103
98
|
packageName: peer,
|
|
104
99
|
packageDirectory: directory,
|
|
105
100
|
currentVersion: current.version,
|
|
106
101
|
})
|
|
107
|
-
if (release
|
|
108
|
-
if (!matches(release
|
|
102
|
+
if (release === undefined) return []
|
|
103
|
+
if (!matches(release, range))
|
|
109
104
|
return [
|
|
110
|
-
`Declared support for ${peer}: ${range} does not include its planned local release ${release
|
|
105
|
+
`Declared support for ${peer}: ${range} does not include its planned local release ${release}. Update the peer range to include the intended version.`,
|
|
111
106
|
]
|
|
112
107
|
if (
|
|
113
108
|
!representatives.some(
|
|
114
109
|
(result) =>
|
|
115
110
|
result.peers[peer]?.source === `candidate` &&
|
|
116
|
-
result.peers[peer]?.version === release
|
|
111
|
+
result.peers[peer]?.version === release &&
|
|
117
112
|
result.suites.some((suite) => suite.peer === peer),
|
|
118
113
|
)
|
|
119
114
|
)
|
|
120
115
|
return [
|
|
121
|
-
`No coordinated representative covers planned local peer ${peer}@${release
|
|
116
|
+
`No coordinated representative covers planned local peer ${peer}@${release}. Enroll ${peer} with breakage.candidates and configure its relationship suite.`,
|
|
122
117
|
]
|
|
123
118
|
return []
|
|
124
119
|
} catch (error) {
|
|
@@ -138,6 +133,7 @@ async function runRepresentative(
|
|
|
138
133
|
directory: string,
|
|
139
134
|
coverageOnly: boolean,
|
|
140
135
|
selection?: NpmTestSelection,
|
|
136
|
+
plannedSuites?: readonly string[],
|
|
141
137
|
): Promise<NpmRepresentativeResult> {
|
|
142
138
|
const label = path.relative(base, directory).split(path.sep).join(`/`)
|
|
143
139
|
const result: NpmRepresentativeResult = {
|
|
@@ -201,6 +197,7 @@ async function runRepresentative(
|
|
|
201
197
|
const peers = Object.keys(config.suites).filter((peer) => {
|
|
202
198
|
const evidence = result.peers[peer]
|
|
203
199
|
if (!evidence) return false
|
|
200
|
+
if (plannedSuites !== undefined) return plannedSuites.includes(peer)
|
|
204
201
|
if (selection && `peer` in selection) return peer === selection.peer
|
|
205
202
|
if (selection && `representative` in selection) return true
|
|
206
203
|
return matches(evidence.version, manifest.peerDependencies![peer]!)
|
|
@@ -262,6 +259,7 @@ async function run(
|
|
|
262
259
|
baseDirectory: string,
|
|
263
260
|
coverageOnly: boolean,
|
|
264
261
|
selection?: NpmTestSelection,
|
|
262
|
+
plannedSuites?: readonly string[],
|
|
265
263
|
): Promise<NpmReport> {
|
|
266
264
|
const report: NpmReport = {
|
|
267
265
|
package: { name: `(unresolved)`, version: `(unresolved)` },
|
|
@@ -297,24 +295,30 @@ async function run(
|
|
|
297
295
|
)
|
|
298
296
|
eras(selection.range)
|
|
299
297
|
}
|
|
300
|
-
// One piece of release evidence per package per invocation, including failures.
|
|
301
|
-
const releases = new Map<string, Promise<
|
|
302
|
-
const
|
|
298
|
+
// One piece of release evidence per package and baseline per invocation, including failures.
|
|
299
|
+
const releases = new Map<string, Promise<VersionTransition | undefined>>()
|
|
300
|
+
const resolveDelta = (
|
|
301
|
+
context: ReleaseDeltaContext,
|
|
302
|
+
): Promise<VersionTransition | undefined> => {
|
|
303
|
+
const key = JSON.stringify([
|
|
304
|
+
context.packageDirectory,
|
|
305
|
+
context.currentVersion,
|
|
306
|
+
context.baseline,
|
|
307
|
+
])
|
|
308
|
+
let pending = releases.get(key)
|
|
309
|
+
if (!pending) {
|
|
310
|
+
pending = resolveReleaseDelta(config.release!, context)
|
|
311
|
+
releases.set(key, pending)
|
|
312
|
+
}
|
|
313
|
+
return pending
|
|
314
|
+
}
|
|
303
315
|
const effective: NpmConfig = {
|
|
304
316
|
...config,
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
pending = Promise.resolve().then(() =>
|
|
309
|
-
configuredHook
|
|
310
|
-
? configuredHook(context)
|
|
311
|
-
: ({ status: `none` } as const),
|
|
312
|
-
)
|
|
313
|
-
releases.set(context.packageDirectory, pending)
|
|
314
|
-
}
|
|
315
|
-
return pending
|
|
316
|
-
},
|
|
317
|
+
...(config.release
|
|
318
|
+
? { release: { ...config.release, resolveDelta } }
|
|
319
|
+
: {}),
|
|
317
320
|
}
|
|
321
|
+
|
|
318
322
|
const directories = await discover(config, base)
|
|
319
323
|
const [support, representatives] = await Promise.all([
|
|
320
324
|
selection
|
|
@@ -329,6 +333,7 @@ async function run(
|
|
|
329
333
|
directory,
|
|
330
334
|
coverageOnly,
|
|
331
335
|
selection,
|
|
336
|
+
plannedSuites,
|
|
332
337
|
),
|
|
333
338
|
),
|
|
334
339
|
),
|
|
@@ -411,6 +416,21 @@ export async function npmTest(
|
|
|
411
416
|
)
|
|
412
417
|
}
|
|
413
418
|
|
|
419
|
+
/** Internal matrix entrypoint: keep the full config for validation and peer identity checks. */
|
|
420
|
+
export async function npmTestPlanned(
|
|
421
|
+
config: NpmConfig,
|
|
422
|
+
selection: { representative: string; suites: readonly string[] },
|
|
423
|
+
options: { baseDirectory?: string } = {},
|
|
424
|
+
): Promise<NpmReport> {
|
|
425
|
+
return await run(
|
|
426
|
+
config,
|
|
427
|
+
options.baseDirectory ?? process.cwd(),
|
|
428
|
+
false,
|
|
429
|
+
{ representative: selection.representative },
|
|
430
|
+
selection.suites,
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
|
|
414
434
|
export function formatNpmReport(report: NpmReport): string {
|
|
415
435
|
const lines = [
|
|
416
436
|
`${report.package.name}@${report.package.version} — npm ${report.mode}`,
|
package/src/npm/cli.ts
CHANGED
|
@@ -1,30 +1,39 @@
|
|
|
1
|
-
import { existsSync } from "node:fs"
|
|
2
|
-
import { readFile } from "node:fs/promises"
|
|
3
1
|
import path from "node:path"
|
|
4
|
-
import { pathToFileURL } from "node:url"
|
|
5
2
|
|
|
6
3
|
import { type } from "arktype"
|
|
7
4
|
import type { OptionsGroup } from "comline"
|
|
8
5
|
import { options, parseBooleanOption } from "comline"
|
|
9
6
|
|
|
7
|
+
import { configOptions, defaultConfig, loadConfig } from "../config.ts"
|
|
8
|
+
|
|
10
9
|
import { formatNpmReport, npmCheck, npmTest } from "./check.ts"
|
|
11
|
-
import { errorMessage } from "
|
|
10
|
+
import { errorMessage } from "../task.ts"
|
|
12
11
|
import { validateConfig } from "./config.ts"
|
|
13
|
-
import { record } from "./manifest.ts"
|
|
14
12
|
import type { NpmTestSelection } from "./types.ts"
|
|
15
13
|
|
|
16
|
-
type CommonOptions = {
|
|
14
|
+
type CommonOptions = {
|
|
15
|
+
config?: string | undefined
|
|
16
|
+
json?: boolean | undefined
|
|
17
|
+
release?: import("../release.ts").ReleaseConfig
|
|
18
|
+
npm?: import("./types.ts").NpmConfig
|
|
19
|
+
npmMatrix?: import("./matrix-model.ts").NpmMatrixConfig
|
|
20
|
+
}
|
|
17
21
|
type CheckOptions = CommonOptions & { coverageOnly?: boolean | undefined }
|
|
18
22
|
type TestOptions = CommonOptions & { representative?: string | undefined }
|
|
19
23
|
|
|
20
24
|
const common = {
|
|
25
|
+
release: configOptions.optionConfigs.release,
|
|
26
|
+
npm: configOptions.optionConfigs.npm,
|
|
27
|
+
npmMatrix: configOptions.optionConfigs.npmMatrix,
|
|
21
28
|
config: {
|
|
29
|
+
schema: type(`string`),
|
|
22
30
|
required: false,
|
|
23
|
-
description: `Configuration file (defaults to breakage.config.ts
|
|
31
|
+
description: `Configuration file (defaults to breakage.config.ts).`,
|
|
24
32
|
example: `--config ./breakage.config.ts`,
|
|
25
33
|
completion: { fileSystem: `files` as const, repeatable: false },
|
|
26
34
|
},
|
|
27
35
|
json: {
|
|
36
|
+
schema: type(`boolean`),
|
|
28
37
|
required: false,
|
|
29
38
|
description: `Emit the structured result.`,
|
|
30
39
|
example: `--json`,
|
|
@@ -34,10 +43,18 @@ const common = {
|
|
|
34
43
|
|
|
35
44
|
export const npmCheckManual: OptionsGroup<CheckOptions> = options(
|
|
36
45
|
`Check npm peer compatibility and changes in declared support.`,
|
|
37
|
-
type({
|
|
46
|
+
type({
|
|
47
|
+
"config?": `string`,
|
|
48
|
+
"json?": `boolean`,
|
|
49
|
+
"coverageOnly?": `boolean`,
|
|
50
|
+
"release?": configOptions.optionsSchema.get(`release`),
|
|
51
|
+
"npm?": configOptions.optionsSchema.get(`npm`),
|
|
52
|
+
"npmMatrix?": configOptions.optionsSchema.get(`npmMatrix`),
|
|
53
|
+
}),
|
|
38
54
|
{
|
|
39
55
|
...common,
|
|
40
56
|
coverageOnly: {
|
|
57
|
+
schema: type(`boolean`),
|
|
41
58
|
aliases: [`coverage-only`],
|
|
42
59
|
required: false,
|
|
43
60
|
description: `Audit coverage and support changes without executing assertions.`,
|
|
@@ -53,10 +70,14 @@ export const npmTestManual: OptionsGroup<TestOptions> = options(
|
|
|
53
70
|
"config?": `string`,
|
|
54
71
|
"json?": `boolean`,
|
|
55
72
|
"representative?": `string`,
|
|
73
|
+
"release?": configOptions.optionsSchema.get(`release`),
|
|
74
|
+
"npm?": configOptions.optionsSchema.get(`npm`),
|
|
75
|
+
"npmMatrix?": configOptions.optionsSchema.get(`npmMatrix`),
|
|
56
76
|
}),
|
|
57
77
|
{
|
|
58
78
|
...common,
|
|
59
79
|
representative: {
|
|
80
|
+
schema: type(`string`),
|
|
60
81
|
required: false,
|
|
61
82
|
description: `Select an exact configured representative path relative to the package.`,
|
|
62
83
|
example: `--representative peer-environments/react-19`,
|
|
@@ -65,15 +86,6 @@ export const npmTestManual: OptionsGroup<TestOptions> = options(
|
|
|
65
86
|
},
|
|
66
87
|
)
|
|
67
88
|
|
|
68
|
-
export function defaultConfig(directory: string): string {
|
|
69
|
-
return path.join(
|
|
70
|
-
directory,
|
|
71
|
-
existsSync(path.join(directory, `breakage.config.ts`))
|
|
72
|
-
? `breakage.config.ts`
|
|
73
|
-
: `breakage.config.json`,
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
89
|
export async function runNpmCli(
|
|
78
90
|
action: `check` | `test`,
|
|
79
91
|
opts: CheckOptions & TestOptions,
|
|
@@ -81,11 +93,7 @@ export async function runNpmCli(
|
|
|
81
93
|
): Promise<number> {
|
|
82
94
|
try {
|
|
83
95
|
const filename = path.resolve(opts.config ?? defaultConfig(process.cwd()))
|
|
84
|
-
const
|
|
85
|
-
? JSON.parse(await readFile(filename, `utf8`))
|
|
86
|
-
: ((await import(pathToFileURL(filename).href)) as { default: unknown })
|
|
87
|
-
.default
|
|
88
|
-
const config = record(loaded) ? loaded.npm : undefined
|
|
96
|
+
const config = (await loadConfig(filename)).npm
|
|
89
97
|
validateConfig(config)
|
|
90
98
|
let selection: NpmTestSelection | undefined
|
|
91
99
|
if (action === `test`) {
|
package/src/npm/config.ts
CHANGED
|
@@ -1,15 +1,45 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
|
+
import { type } from "arktype"
|
|
3
|
+
|
|
4
|
+
import { releaseSchema, rejectCertificationOptions } from "../release.ts"
|
|
2
5
|
|
|
3
6
|
import { eras, packageName, record } from "./manifest.ts"
|
|
4
7
|
import type { NpmConfig } from "./types.ts"
|
|
5
8
|
|
|
9
|
+
export const npmSuiteSchema = type({
|
|
10
|
+
"+": `reject`,
|
|
11
|
+
files: `string[]`,
|
|
12
|
+
command: `string`,
|
|
13
|
+
})
|
|
14
|
+
export const npmConfigSchema = type({
|
|
15
|
+
"+": `reject`,
|
|
16
|
+
"release?": releaseSchema,
|
|
17
|
+
"packageDirectory?": `string`,
|
|
18
|
+
artifact: `string`,
|
|
19
|
+
representatives: `string | string[]`,
|
|
20
|
+
lockfile: `string`,
|
|
21
|
+
prepareCommand: `string`,
|
|
22
|
+
"installCommand?": `string`,
|
|
23
|
+
suites: { "[string]": npmSuiteSchema },
|
|
24
|
+
"localPeers?": {
|
|
25
|
+
"[string]": { "+": `reject`, directory: `string`, artifact: `string` },
|
|
26
|
+
},
|
|
27
|
+
baseline: type({ "+": `reject`, firstRelease: `true` }).or({
|
|
28
|
+
"+": `reject`,
|
|
29
|
+
"tagPattern?": `string`,
|
|
30
|
+
"baselineFile?": `string`,
|
|
31
|
+
}),
|
|
32
|
+
})
|
|
33
|
+
|
|
6
34
|
function nonempty(value: unknown): value is string {
|
|
7
35
|
return typeof value === `string` && value.trim().length > 0
|
|
8
36
|
}
|
|
9
37
|
|
|
10
38
|
export function validateConfig(value: unknown): asserts value is NpmConfig {
|
|
11
39
|
if (!record(value)) throw new Error(`Configuration must have an npm object.`)
|
|
12
|
-
|
|
40
|
+
rejectCertificationOptions(value)
|
|
41
|
+
npmConfigSchema.assert(value)
|
|
42
|
+
for (const key of [`artifact`, `lockfile`] as const) {
|
|
13
43
|
if (!nonempty(value[key]))
|
|
14
44
|
throw new Error(`npm.${key} must be a nonempty string.`)
|
|
15
45
|
}
|
|
@@ -19,7 +49,13 @@ export function validateConfig(value: unknown): asserts value is NpmConfig {
|
|
|
19
49
|
) {
|
|
20
50
|
throw new Error(`npm.lockfile must stay inside each representative.`)
|
|
21
51
|
}
|
|
22
|
-
|
|
52
|
+
if (!nonempty(value.prepareCommand))
|
|
53
|
+
throw new Error(
|
|
54
|
+
`npm.prepareCommand must be a nonempty shell command string.`,
|
|
55
|
+
)
|
|
56
|
+
if (value.installCommand !== undefined && !nonempty(value.installCommand))
|
|
57
|
+
throw new Error(`Invalid npm.installCommand.`)
|
|
58
|
+
for (const key of [`packageDirectory`] as const) {
|
|
23
59
|
if (value[key] !== undefined && !nonempty(value[key]))
|
|
24
60
|
throw new Error(`Invalid npm.${key}.`)
|
|
25
61
|
}
|
|
@@ -75,10 +111,6 @@ export function validateConfig(value: unknown): asserts value is NpmConfig {
|
|
|
75
111
|
}
|
|
76
112
|
}
|
|
77
113
|
}
|
|
78
|
-
for (const hook of [`releaseIntent`, `certifyBreaking`] as const) {
|
|
79
|
-
if (value[hook] !== undefined && typeof value[hook] !== `function`)
|
|
80
|
-
throw new Error(`npm.${hook} must be a function.`)
|
|
81
|
-
}
|
|
82
114
|
}
|
|
83
115
|
|
|
84
116
|
export function validatePeerRanges(peers: Record<string, string>): void {
|
package/src/npm/environment.ts
CHANGED
|
@@ -16,11 +16,11 @@ import path from "node:path"
|
|
|
16
16
|
import semver from "semver"
|
|
17
17
|
import { simpleGit } from "simple-git"
|
|
18
18
|
|
|
19
|
-
import { prepare } from "
|
|
19
|
+
import { prepare } from "../task.ts"
|
|
20
20
|
import { validatePeerRanges } from "./config.ts"
|
|
21
21
|
import type { Manifest } from "./manifest.ts"
|
|
22
22
|
import { matches, normalizeWorkspacePeers, readManifest } from "./manifest.ts"
|
|
23
|
-
import {
|
|
23
|
+
import { releaseVersion } from "./release.ts"
|
|
24
24
|
import type { NpmConfig, NpmPeerEvidence, NpmSuite } from "./types.ts"
|
|
25
25
|
|
|
26
26
|
export type Environment = {
|
|
@@ -192,14 +192,14 @@ export async function maySelectEnvironment(
|
|
|
192
192
|
)
|
|
193
193
|
if (current.name !== peer)
|
|
194
194
|
throw new Error(`Local peer identity mismatch for ${peer}.`)
|
|
195
|
-
const release = await
|
|
195
|
+
const release = await releaseVersion(config, {
|
|
196
196
|
packageName: peer,
|
|
197
197
|
packageDirectory,
|
|
198
198
|
currentVersion: current.version,
|
|
199
199
|
})
|
|
200
|
-
if (release
|
|
201
|
-
throw new Error(`Candidate ${peer} has no
|
|
202
|
-
return matches(release
|
|
200
|
+
if (release === undefined)
|
|
201
|
+
throw new Error(`Candidate ${peer} has no release version.`)
|
|
202
|
+
return matches(release, range)
|
|
203
203
|
}
|
|
204
204
|
const declared = declarations[peer]
|
|
205
205
|
return declared !== undefined && matches(declared, range)
|
|
@@ -243,18 +243,18 @@ export async function prepareEnvironment(
|
|
|
243
243
|
if (localManifest.name !== peer)
|
|
244
244
|
throw new Error(`Local peer identity mismatch for ${peer}.`)
|
|
245
245
|
validatePeerRanges(localManifest.peerDependencies ?? {})
|
|
246
|
-
const evidence = await
|
|
246
|
+
const evidence = await releaseVersion(config, {
|
|
247
247
|
packageName: peer,
|
|
248
248
|
packageDirectory: localDirectory,
|
|
249
249
|
currentVersion: localManifest.version,
|
|
250
250
|
})
|
|
251
|
-
if (evidence
|
|
252
|
-
throw new Error(`Candidate ${peer} has no
|
|
251
|
+
if (evidence === undefined)
|
|
252
|
+
throw new Error(`Candidate ${peer} has no release version.`)
|
|
253
253
|
candidates[peer] = {
|
|
254
254
|
directory: localDirectory,
|
|
255
255
|
artifact: path.resolve(base, local.artifact),
|
|
256
256
|
currentVersion: localManifest.version,
|
|
257
|
-
version: evidence
|
|
257
|
+
version: evidence,
|
|
258
258
|
manifest: localManifest,
|
|
259
259
|
}
|
|
260
260
|
}
|