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
package/src/breakage.x.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import path from "node:path"
|
|
3
4
|
import { inspect } from "node:util"
|
|
4
5
|
|
|
5
|
-
import { type } from "arktype"
|
|
6
|
-
import type { OptionsGroup } from "comline"
|
|
7
6
|
import {
|
|
8
7
|
cli,
|
|
9
8
|
completionResponse,
|
|
@@ -18,28 +17,29 @@ import {
|
|
|
18
17
|
} from "comline"
|
|
19
18
|
import logger from "takua"
|
|
20
19
|
|
|
21
|
-
import type { BreakageOptions } from "./breakage.ts"
|
|
22
|
-
import { breakage, breakagePrelude } from "./breakage.ts"
|
|
23
20
|
import {
|
|
24
21
|
defaultConfig,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
configPath,
|
|
23
|
+
resolveConfig,
|
|
24
|
+
configSchema,
|
|
25
|
+
configFields,
|
|
26
|
+
} from "./config.ts"
|
|
27
|
+
import { breakage, breakagePrelude } from "./breakage.ts"
|
|
28
|
+
import { errorMessage } from "./task.ts"
|
|
29
|
+
import { npmCheckManual, npmTestManual, runNpmCli } from "./npm/cli.ts"
|
|
30
|
+
|
|
31
|
+
// Either command spelling can override the other spelling in a config file.
|
|
32
|
+
const commandOverrides = new Set<`test` | `testCommand`>()
|
|
29
33
|
|
|
30
34
|
const BREAKAGE_MANUAL = options(
|
|
31
35
|
`Check for breaking changes in a package.`,
|
|
32
|
-
|
|
33
|
-
"tagPattern?": `string`,
|
|
34
|
-
"baselineFile?": `string`,
|
|
35
|
-
testPattern: `string`,
|
|
36
|
-
testCommand: `string`,
|
|
37
|
-
certifyCommand: `string`,
|
|
38
|
-
"baseDirname?": `string`,
|
|
39
|
-
"verbose?": `boolean`,
|
|
40
|
-
}),
|
|
36
|
+
configSchema,
|
|
41
37
|
{
|
|
38
|
+
release: { configOnly: true },
|
|
39
|
+
npm: { configOnly: true },
|
|
40
|
+
out: { configOnly: true },
|
|
42
41
|
tagPattern: {
|
|
42
|
+
schema: configFields.tagPattern,
|
|
43
43
|
aliases: [`tag-pattern`],
|
|
44
44
|
completion: { repeatable: false },
|
|
45
45
|
flag: `g`,
|
|
@@ -48,6 +48,7 @@ const BREAKAGE_MANUAL = options(
|
|
|
48
48
|
example: `--tagPattern="my-library"`,
|
|
49
49
|
},
|
|
50
50
|
baselineFile: {
|
|
51
|
+
schema: configFields.baselineFile,
|
|
51
52
|
aliases: [`baseline-file`],
|
|
52
53
|
completion: { repeatable: false, fileSystem: `files` },
|
|
53
54
|
required: false,
|
|
@@ -55,30 +56,41 @@ const BREAKAGE_MANUAL = options(
|
|
|
55
56
|
example: `--baseline-file=.breakage/baseline.json`,
|
|
56
57
|
},
|
|
57
58
|
testPattern: {
|
|
59
|
+
schema: configFields.testPattern,
|
|
58
60
|
aliases: [`test-pattern`, `pattern`],
|
|
59
61
|
completion: { repeatable: false },
|
|
60
62
|
flag: `p`,
|
|
61
|
-
required:
|
|
63
|
+
required: false,
|
|
62
64
|
description: `The pattern to match test files that test the public API of the library.`,
|
|
63
65
|
example: `--pattern="*__public.test.ts"`,
|
|
64
66
|
},
|
|
65
67
|
testCommand: {
|
|
68
|
+
parse: (value) => {
|
|
69
|
+
commandOverrides.add(`testCommand`)
|
|
70
|
+
return value
|
|
71
|
+
},
|
|
72
|
+
schema: configFields.testCommand,
|
|
66
73
|
aliases: [`test-command`],
|
|
67
74
|
completion: { repeatable: false },
|
|
68
75
|
flag: `t`,
|
|
69
|
-
required:
|
|
76
|
+
required: false,
|
|
70
77
|
description: `Complete bash command that runs the tests for the library's public API.`,
|
|
71
78
|
example: `--testCommand="npm run test"`,
|
|
72
79
|
},
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
test: {
|
|
81
|
+
parse: (value) => {
|
|
82
|
+
commandOverrides.add(`test`)
|
|
83
|
+
return value
|
|
84
|
+
},
|
|
85
|
+
schema: configFields.test,
|
|
75
86
|
completion: { repeatable: false },
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
example: `--certifyCommand=\tsx scripts/certify-major-version-bump.node`,
|
|
87
|
+
required: false,
|
|
88
|
+
description: `Shell command running the restored public contracts.`,
|
|
89
|
+
example: `--test="npm run test:public"`,
|
|
80
90
|
},
|
|
81
91
|
baseDirname: {
|
|
92
|
+
parse: (value) => path.resolve(value),
|
|
93
|
+
schema: configFields.baseDirname,
|
|
82
94
|
aliases: [`base-dir`, `base-dirname`],
|
|
83
95
|
completion: { repeatable: false, fileSystem: `directories` },
|
|
84
96
|
flag: `d`,
|
|
@@ -87,6 +99,7 @@ const BREAKAGE_MANUAL = options(
|
|
|
87
99
|
example: `--baseDirname=./packages/my-package`,
|
|
88
100
|
},
|
|
89
101
|
verbose: {
|
|
102
|
+
schema: configFields.verbose,
|
|
90
103
|
completion: { repeatable: false },
|
|
91
104
|
flag: `v`,
|
|
92
105
|
required: false,
|
|
@@ -95,20 +108,11 @@ const BREAKAGE_MANUAL = options(
|
|
|
95
108
|
parse: parseBooleanOption,
|
|
96
109
|
},
|
|
97
110
|
},
|
|
98
|
-
)
|
|
111
|
+
)
|
|
99
112
|
|
|
100
113
|
const PRELUDE_MANUAL = options(
|
|
101
114
|
`Resolve and fetch the release baseline for a cacheable compatibility check.`,
|
|
102
|
-
|
|
103
|
-
"tagPattern?": `string`,
|
|
104
|
-
"baseDirname?": `string`,
|
|
105
|
-
"out?": `string`,
|
|
106
|
-
"testPattern?": `string`,
|
|
107
|
-
"testCommand?": `string`,
|
|
108
|
-
"certifyCommand?": `string`,
|
|
109
|
-
"baselineFile?": `string`,
|
|
110
|
-
"verbose?": `boolean`,
|
|
111
|
-
}),
|
|
115
|
+
configSchema,
|
|
112
116
|
{
|
|
113
117
|
...BREAKAGE_MANUAL.optionConfigs,
|
|
114
118
|
testPattern: {
|
|
@@ -119,11 +123,8 @@ const PRELUDE_MANUAL = options(
|
|
|
119
123
|
...BREAKAGE_MANUAL.optionConfigs.testCommand,
|
|
120
124
|
required: false,
|
|
121
125
|
},
|
|
122
|
-
certifyCommand: {
|
|
123
|
-
...BREAKAGE_MANUAL.optionConfigs.certifyCommand,
|
|
124
|
-
required: false,
|
|
125
|
-
},
|
|
126
126
|
out: {
|
|
127
|
+
schema: configFields.out,
|
|
127
128
|
completion: { repeatable: false, fileSystem: `files` },
|
|
128
129
|
required: false,
|
|
129
130
|
description: `Gitignored snapshot path relative to the base directory (default: .breakage/baseline.json).`,
|
|
@@ -132,27 +133,21 @@ const PRELUDE_MANUAL = options(
|
|
|
132
133
|
},
|
|
133
134
|
)
|
|
134
135
|
|
|
135
|
-
|
|
136
|
-
`Create a copy of the schema for configuring breakage.`,
|
|
137
|
-
type({ "outdir?": `string` }),
|
|
138
|
-
{
|
|
139
|
-
outdir: {
|
|
140
|
-
aliases: [`out-dir`],
|
|
141
|
-
completion: { repeatable: false, fileSystem: `directories` },
|
|
142
|
-
flag: `o`,
|
|
143
|
-
required: false,
|
|
144
|
-
description: `Directory to write the schema to.`,
|
|
145
|
-
example: `--outdir=./dist`,
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
|
-
)
|
|
149
|
-
|
|
136
|
+
let loadedConfigPath: string | undefined
|
|
150
137
|
const parse = cli(
|
|
151
138
|
{
|
|
152
139
|
cliName: `breakage`,
|
|
140
|
+
discoverConfigPath: (segments: readonly string[]) => {
|
|
141
|
+
if (segments[0] === `help` || segments[0] === `npm`) return undefined
|
|
142
|
+
const explicit = segments[0] === `prelude` ? segments[1] : segments[0]
|
|
143
|
+
loadedConfigPath = configPath(
|
|
144
|
+
explicit ?? defaultConfig(process.cwd()),
|
|
145
|
+
explicit !== undefined,
|
|
146
|
+
)
|
|
147
|
+
return loadedConfigPath
|
|
148
|
+
},
|
|
153
149
|
routes: optional({
|
|
154
150
|
help: null,
|
|
155
|
-
schema: null,
|
|
156
151
|
npm: optional({
|
|
157
152
|
help: null,
|
|
158
153
|
check: null,
|
|
@@ -165,7 +160,6 @@ const parse = cli(
|
|
|
165
160
|
"": BREAKAGE_MANUAL,
|
|
166
161
|
$configPath: BREAKAGE_MANUAL,
|
|
167
162
|
help: noOptions(`Show usage.`),
|
|
168
|
-
schema: SCHEMA_MANUAL,
|
|
169
163
|
npm: noOptions(`Show npm compatibility commands.`),
|
|
170
164
|
"npm/help": noOptions(`Show npm compatibility commands.`),
|
|
171
165
|
"npm/check": npmCheckManual,
|
|
@@ -178,15 +172,6 @@ const parse = cli(
|
|
|
178
172
|
$configPath: { fileSystem: `files` },
|
|
179
173
|
"prelude/$configPath": { fileSystem: `files` },
|
|
180
174
|
},
|
|
181
|
-
discoverConfigPath: (args: readonly string[]) => {
|
|
182
|
-
if (args[0] === `help` || args[0] === `schema` || args[0] === `npm`) {
|
|
183
|
-
return
|
|
184
|
-
}
|
|
185
|
-
const configPath =
|
|
186
|
-
(args[0] === `prelude` ? args[1] : args[0]) ??
|
|
187
|
-
defaultConfig(process.cwd())
|
|
188
|
-
return configPath
|
|
189
|
-
},
|
|
190
175
|
},
|
|
191
176
|
{
|
|
192
177
|
info: (message, ...data) => logger.info(`breakage`, message, data),
|
|
@@ -198,74 +183,110 @@ const completion = await completionResponse(parse.definition, process.argv)
|
|
|
198
183
|
if (completion !== undefined) {
|
|
199
184
|
process.stdout.write(completion)
|
|
200
185
|
} else {
|
|
201
|
-
|
|
202
|
-
|
|
186
|
+
try {
|
|
187
|
+
const { inputs, warnings } = await parse.async(process.argv)
|
|
188
|
+
logWarnings(warnings)
|
|
189
|
+
if (
|
|
190
|
+
process.argv
|
|
191
|
+
.slice(2)
|
|
192
|
+
.some((arg) =>
|
|
193
|
+
/^(?:--certifyCommand|--certify-command|-c)(?:=|$)/.test(arg),
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
throw new Error(
|
|
197
|
+
`certifyCommand was removed. Configure release.resolveDelta and release.policy instead.`,
|
|
198
|
+
)
|
|
203
199
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
{
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const baseline = await breakagePrelude({
|
|
229
|
-
...inputs.opts,
|
|
230
|
-
out: inputs.opts.out ?? `.breakage/baseline.json`,
|
|
231
|
-
})
|
|
232
|
-
process.stdout.write(`Pinned ${baseline.ref} at ${baseline.commit}.\n`)
|
|
233
|
-
break
|
|
234
|
-
}
|
|
235
|
-
case ``:
|
|
236
|
-
case `$configPath`: {
|
|
237
|
-
const { returnValue } = await encapsulate(() => breakage(inputs.opts), {
|
|
238
|
-
console: true,
|
|
239
|
-
stdout: true,
|
|
240
|
-
})
|
|
241
|
-
if (`testResult` in returnValue && returnValue.breakingChangesFound) {
|
|
242
|
-
process.stdout.write(returnValue.testResult)
|
|
200
|
+
switch (inputs.case) {
|
|
201
|
+
case `npm`:
|
|
202
|
+
case `npm/help`:
|
|
203
|
+
case `help`:
|
|
204
|
+
console.log(help(parse.definition))
|
|
205
|
+
break
|
|
206
|
+
case `npm/check`:
|
|
207
|
+
process.exitCode = await runNpmCli(`check`, inputs.opts)
|
|
208
|
+
break
|
|
209
|
+
case `npm/test`:
|
|
210
|
+
process.exitCode = await runNpmCli(`test`, inputs.opts)
|
|
211
|
+
break
|
|
212
|
+
case `npm/test/$peer/$range`:
|
|
213
|
+
process.exitCode = await runNpmCli(`test`, inputs.opts, inputs.params)
|
|
214
|
+
break
|
|
215
|
+
case `prelude`:
|
|
216
|
+
case `prelude/$configPath`: {
|
|
217
|
+
const config = resolveConfig(inputs.opts, loadedConfigPath)
|
|
218
|
+
const baseline = await breakagePrelude({
|
|
219
|
+
...config,
|
|
220
|
+
out: config.out ?? `.breakage/baseline.json`,
|
|
221
|
+
})
|
|
222
|
+
process.stdout.write(`Pinned ${baseline.ref} at ${baseline.commit}.\n`)
|
|
223
|
+
break
|
|
243
224
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
225
|
+
case ``:
|
|
226
|
+
case `$configPath`: {
|
|
227
|
+
const merged = resolveConfig(inputs.opts, loadedConfigPath)
|
|
228
|
+
if (
|
|
229
|
+
commandOverrides.has(`testCommand`) &&
|
|
230
|
+
!commandOverrides.has(`test`)
|
|
231
|
+
)
|
|
232
|
+
delete merged.test
|
|
233
|
+
if (
|
|
234
|
+
commandOverrides.has(`test`) &&
|
|
235
|
+
!commandOverrides.has(`testCommand`)
|
|
236
|
+
)
|
|
237
|
+
delete merged.testCommand
|
|
238
|
+
if (!merged.testPattern)
|
|
239
|
+
throw new Error(`required arguments were not provided: testPattern.`)
|
|
240
|
+
const { returnValue } = await encapsulate(
|
|
241
|
+
() =>
|
|
242
|
+
breakage({
|
|
243
|
+
...merged,
|
|
244
|
+
testPattern: merged.testPattern!,
|
|
245
|
+
}),
|
|
246
|
+
{
|
|
247
|
+
console: true,
|
|
248
|
+
stdout: true,
|
|
249
|
+
},
|
|
250
|
+
)
|
|
251
|
+
if (`release` in returnValue && returnValue.release) {
|
|
252
|
+
const { previousVersion, version, delta, breaking } =
|
|
253
|
+
returnValue.release
|
|
254
|
+
process.stdout.write(
|
|
255
|
+
`Release: ${previousVersion} → ${version} (${delta}; ${breaking ? `breaking` : `compatible`}).\n`,
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
if (`testResult` in returnValue && returnValue.breakingChangesFound) {
|
|
259
|
+
process.stdout.write(returnValue.testResult)
|
|
260
|
+
}
|
|
261
|
+
if (`breakingChangesFound` in returnValue) {
|
|
262
|
+
if (returnValue.breakingChangesFound) {
|
|
263
|
+
if (returnValue.release?.breaking) {
|
|
264
|
+
process.stdout.write(
|
|
265
|
+
`👷 Breaking changes are covered by the breaking release.`,
|
|
266
|
+
)
|
|
267
|
+
process.exit(0)
|
|
268
|
+
} else {
|
|
269
|
+
process.stdout.write(
|
|
270
|
+
`❌ Breaking changes require a breaking release.`,
|
|
271
|
+
)
|
|
272
|
+
process.exit(1)
|
|
273
|
+
}
|
|
251
274
|
} else {
|
|
252
|
-
process.stdout.write(
|
|
253
|
-
|
|
254
|
-
)
|
|
255
|
-
process.exit(1)
|
|
275
|
+
process.stdout.write(`✅ No breaking changes were found.`)
|
|
276
|
+
process.exit(0)
|
|
256
277
|
}
|
|
257
278
|
} else {
|
|
258
|
-
process.stdout.write(
|
|
259
|
-
|
|
279
|
+
process.stdout.write(
|
|
280
|
+
`💥 Breakage failed to determine breaking changes.` +
|
|
281
|
+
`\n` +
|
|
282
|
+
inspect(returnValue),
|
|
283
|
+
)
|
|
284
|
+
process.exit(2)
|
|
260
285
|
}
|
|
261
|
-
} else {
|
|
262
|
-
process.stdout.write(
|
|
263
|
-
`💥 Breakage failed to determine breaking changes.` +
|
|
264
|
-
`\n` +
|
|
265
|
-
inspect(returnValue),
|
|
266
|
-
)
|
|
267
|
-
process.exit(2)
|
|
268
286
|
}
|
|
269
287
|
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
process.stderr.write(`Breakage setup error: ${errorMessage(error)}\n`)
|
|
290
|
+
process.exitCode = 2
|
|
270
291
|
}
|
|
271
292
|
}
|
|
@@ -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,128 @@
|
|
|
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 {
|
|
9
|
+
releaseSchema,
|
|
10
|
+
validateRelease,
|
|
11
|
+
rejectCertificationOptions,
|
|
12
|
+
} from "./release.ts"
|
|
13
|
+
|
|
14
|
+
// Reuse these field schemas for CLI metadata even though hooks cannot become JSON Schema.
|
|
15
|
+
export const configFields = {
|
|
16
|
+
tagPattern: type(`string`),
|
|
17
|
+
baselineFile: type(`string`),
|
|
18
|
+
testPattern: type(`string`),
|
|
19
|
+
testCommand: type(`string`),
|
|
20
|
+
test: type(`string`),
|
|
21
|
+
baseDirname: type(`string`),
|
|
22
|
+
verbose: type(`boolean`),
|
|
23
|
+
out: type(`string`),
|
|
24
|
+
}
|
|
25
|
+
export const configSchema = type({
|
|
26
|
+
"+": `reject`,
|
|
27
|
+
"tagPattern?": configFields.tagPattern.or(`undefined`),
|
|
28
|
+
"baselineFile?": configFields.baselineFile.or(`undefined`),
|
|
29
|
+
"testPattern?": configFields.testPattern,
|
|
30
|
+
"testCommand?": configFields.testCommand,
|
|
31
|
+
"test?": configFields.test,
|
|
32
|
+
"baseDirname?": configFields.baseDirname,
|
|
33
|
+
"verbose?": configFields.verbose.or(`undefined`),
|
|
34
|
+
"out?": configFields.out,
|
|
35
|
+
"release?": releaseSchema,
|
|
36
|
+
"npm?": npmConfigSchema,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/** The complete config, with no fields exposed to argv by this loader. */
|
|
40
|
+
export const configOptions = options(`Breakage configuration`, configSchema, {
|
|
41
|
+
tagPattern: { configOnly: true },
|
|
42
|
+
baselineFile: { configOnly: true },
|
|
43
|
+
testPattern: { configOnly: true },
|
|
44
|
+
testCommand: { configOnly: true },
|
|
45
|
+
test: { configOnly: true },
|
|
46
|
+
baseDirname: { configOnly: true },
|
|
47
|
+
verbose: { configOnly: true },
|
|
48
|
+
out: { configOnly: true },
|
|
49
|
+
release: { configOnly: true },
|
|
50
|
+
npm: { configOnly: true },
|
|
51
|
+
})
|
|
52
|
+
export type BreakageConfig = ConfigInput<typeof configOptions>
|
|
53
|
+
|
|
54
|
+
/** Share the root release model with npm checks unless they supply their own. */
|
|
55
|
+
export function defineConfig(config: BreakageConfig): BreakageConfig {
|
|
56
|
+
if (config && typeof config === `object`) {
|
|
57
|
+
rejectCertificationOptions(config)
|
|
58
|
+
if (config.npm) rejectCertificationOptions(config.npm)
|
|
59
|
+
if (config.release !== undefined) validateRelease(config.release)
|
|
60
|
+
if (config.npm?.release !== undefined) validateRelease(config.npm.release)
|
|
61
|
+
}
|
|
62
|
+
configSchema.assert(config)
|
|
63
|
+
return {
|
|
64
|
+
...config,
|
|
65
|
+
...(config.npm
|
|
66
|
+
? {
|
|
67
|
+
npm: {
|
|
68
|
+
...config.npm,
|
|
69
|
+
...(config.release && config.npm.release === undefined
|
|
70
|
+
? { release: config.release }
|
|
71
|
+
: {}),
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
: {}),
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function defaultConfig(directory: string): string {
|
|
79
|
+
const filename = path.join(directory, `breakage.config.ts`)
|
|
80
|
+
if (
|
|
81
|
+
!existsSync(filename) &&
|
|
82
|
+
existsSync(path.join(directory, `breakage.config.json`))
|
|
83
|
+
)
|
|
84
|
+
throw new Error(
|
|
85
|
+
`JSON configuration was removed. Migrate breakage.config.json to breakage.config.ts.`,
|
|
86
|
+
)
|
|
87
|
+
return filename
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Comline ignores absent discovered files; explicit paths must exist. */
|
|
91
|
+
export function configPath(filename: string, required = true): string {
|
|
92
|
+
const absolute = path.resolve(filename)
|
|
93
|
+
if (path.extname(absolute) !== `.ts`)
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Configuration must be a .ts module with a default export; JSON configuration was removed.`,
|
|
96
|
+
)
|
|
97
|
+
if (required && !existsSync(absolute))
|
|
98
|
+
throw new Error(`Configuration file does not exist: ${absolute}`)
|
|
99
|
+
return absolute
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Apply application semantics after Comline validates and merges configuration. */
|
|
103
|
+
export function resolveConfig(
|
|
104
|
+
config: BreakageConfig,
|
|
105
|
+
filename?: string,
|
|
106
|
+
): BreakageConfig {
|
|
107
|
+
return {
|
|
108
|
+
...defineConfig(config),
|
|
109
|
+
baseDirname: path.resolve(
|
|
110
|
+
filename ? path.dirname(filename) : process.cwd(),
|
|
111
|
+
config.baseDirname ?? `.`,
|
|
112
|
+
),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Use Comline's module loading and full-schema validation for programmatic and npm consumers. */
|
|
117
|
+
export async function loadConfig(filename: string): Promise<BreakageConfig> {
|
|
118
|
+
const absolute = configPath(filename)
|
|
119
|
+
const parse = cli({
|
|
120
|
+
cliName: `breakage`,
|
|
121
|
+
routeOptions: { "": configOptions },
|
|
122
|
+
discoverConfigPath: () => absolute,
|
|
123
|
+
})
|
|
124
|
+
return resolveConfig(
|
|
125
|
+
(await parse.async([process.execPath, `breakage`])).inputs.opts,
|
|
126
|
+
absolute,
|
|
127
|
+
)
|
|
128
|
+
}
|